// Written 2008, 2010 by Jan Bennewitz jan.bennewitz@gmail.com

/// <reference path="google-maps-3-vs-1-0.js" />

window.onresize = resize;

var map; // the google map

var types;
var shadowImage = new google.maps.MarkerImage('Images/mm_20_shadow.png', null, null, new google.maps.Point(6, 20));

// [i] (Array):
var markers = [];
// [name]:
var markerIndexByName = []; // numeric index of the named marker

var hoveringMarker = null; // index of hovered marker - null means none
var hoveringShape = null; // shape, if any, the pointer is over
var pinnedMarker = null; // index of pinned marker - null means none
var detailIdle = "<h3>Legend</h3><p>Toggle which items are shown by clicking on them in the legend (upper right of page).</p><h3>Sites, schools and clubs</h3>Move your mouse over any site, club or school on the map or in the list:<ul><li>Hover over it to show its details.</li><li>Click on it to <em>pin</em> it, click anywhere else on the map to <em>unpin</em>.</li><li>Double-click on it to open its site guide entry or home page in a new window.</li></ul><h3>CAR 166 aerodromes</h3><p>Warning: Showing CAR 166 aerodromes is extremely slow on Internet Explorer 8 and earlier.</p><p>When showing CAR 166 aerodromes, a 10nm radius circle is drawn. Double-click on the circle to show the relevant FAC.</p><p>Please note once again that no claim as to accuracy or otherwise is made.</p>";

var urlParams; // has .Center, .Zoom, .Span, .Pin, .Types, .Query
var geocodeSuccess = false; // indicates if q ('query') url param was successfully geocoded

var sitesListJustScrolled = false; // used to switch off the onmouseover event on LI items while the sites-list changes
var doubleClickDeadline; // milliseconds - used to check for double-clicks on sites-list LI elements
var doubleClickInterval = 250; // time in ms within which two clicks count as a double-click
var clickTimeoutId; // id of the click handler timeout - use a delay of doubleClickInterval to allow for possible double-clicks
var openSitePageCooldown = 0; // openSitePage is inactive for a cooldown time after firing to deal with different browsers firing click and doubleclick events differently
var loadingMarkerFiles; // Count of currently still loading marker data files
var aerodromes; // Holds the CAR166 aerodromes data (from json).

var maxZIndex = 1E9; // TODO:check if better solution available

var markersData = [];

var typesSortOrder = [];
typesSortOrder['school'] = 0;
typesSortOrder['club'] = 1;
typesSortOrder['open'] = 2;
typesSortOrder['closed'] = 3;
typesSortOrder['microlight'] = 4;

var shapeStyle = [];
shapeStyle['Landing'] = { dimensions: 2, color: "#00ff00" };
shapeStyle['No Landing'] = { dimensions: 2, color: "#ff0000" };
shapeStyle['Emergency Landing'] = { dimensions: 2, color: "#ff7f00" };
shapeStyle['No Fly Zone'] = { dimensions: 2, color: "#ff0000" };
shapeStyle['Hazard'] = { dimensions: 2, color: "#ff0000" };
shapeStyle['Feature'] = { dimensions: 2, color: "#0000ff" };
shapeStyle['Access'] = { dimensions: 1, color: "#0000ff" };
shapeStyle['Powerline'] = { dimensions: 1, color: "#ff0000" };

function load() {
    types = [];
    types['school'] = getStandardTypeFromColor("orange");
    types['club'] = getStandardTypeFromColor("yellow");
    types['open'] = getStandardTypeFromColor("green");
    types['microlight'] = getStandardTypeFromColor("red");
    types['closed'] = getStandardTypeFromColor("gray");
    types['car166'] = {
        image: 'Images/OrangeDisk.png',
        inactiveImage: 'Images/FadedDisk.png',
        isHidden: true
    };

    resize(); // set initial div sizes

    urlParams = getUrlParams();

    //set which types are shown
    var initialTypes;
    if (urlParams.Types)
        initialTypes = urlParams.Types;
    else
        initialTypes = ['school', 'club', 'open', 'microlight', 'closed']; //default if unspecified

    for (i = 0; i < initialTypes.length; i++) {
        if (types[initialTypes[i].toLowerCase()])
            types[initialTypes[i].toLowerCase()].isHidden = false;
    }

    map = new google.maps.Map(
        $("map"),
        {
            zoom: 4,
            center: new google.maps.LatLng(-25, 136), // arbitrary default position to init map
            mapTypeId: google.maps.MapTypeId.TERRAIN,
            disableDoubleClickZoom: true,
            scaleControl: true
        }
        );

    google.maps.event.addListener(map, 'click', clearPinned); // clicking anywhere on the map unpins the currently pinned marker
    google.maps.event.addListener(map, 'bounds_changed', mapMoved);

    loadingMarkerFiles = 3;
    downloadUrl("schools.csv", function (request) { loadSchoolsData(request.responseText) }); // rewrite to https://spreadsheets.google.com/pub?hl=en&hl=en&key=0Arr1c-bJxr2idEh1Vld2OFRlT0lJYVh5bU54RS0tTlE&single=true&gid=0&output=csv
    downloadUrl("clubs.csv", function (request) { loadClubsData(request.responseText) }); // rewrite to https://spreadsheets.google.com/pub?hl=en&hl=en&key=0Arr1c-bJxr2idEh1Vld2OFRlT0lJYVh5bU54RS0tTlE&single=true&gid=1&output=csv
    downloadUrl("sitesForGMaps.xml", function (request) { loadSitesData(request.responseXML) });

    // set initial viewport if specified by url params (if not, we'll show all markers once they're loaded)
    if (urlParams.Center) {
        if (urlParams.Zoom) {
            map.setCenter(urlParams.Center);
            map.setZoom(urlParams.Zoom);
        } else if (urlParams.Span)
            setMapCenterAndSpan(urlParams.Center, urlParams.Span);
        else {
            map.setCenter(urlParams.Center);
            map.setZoom(8); // default if just center specified
        }
    }

    if (urlParams.Query) {
        var geocoder = new google.maps.Geocoder();
        geocoder.geocode({ 'address': urlParams.Query, 'region': 'AU' }, function (results, status) {
            if (status == google.maps.GeocoderStatus.OK) {
                map.fitBounds(results[0].geometry.viewport);
                geocodeSuccess = true;
            } else {
                alert("Could not geocode the location '" + urlParams.Query + "' for the following reason: " + status);
            }
        });
    }

    var typeToggles = $('show-types').getElementsByTagName('li');
    for (i = 0; i < typeToggles.length; i++) {
        typeToggles[i].onclick = function () { toggleTypeVis(this.id) };
        typeToggles[i].onmouseover = function () { this.className = 'highlight' };
        typeToggles[i].onmouseout = function () { this.className = '' };
        setTypeHeaderImage(typeToggles[i].id);
    }

    showHideCAR166();
}

function unload() {
    //  saveState();
}

function getStandardTypeFromColor(color) {
    return {
        image: 'Images/mm_20_' + color + '.png', // default icon image for this type of site
        highlightImage: 'Images/mm_20_' + color + '_hi.png', // highlighted icon image for this type of site
        fadeImage: 'Images/mm_20_' + color + '_fade.png', // icon image for this type of site when not visible on map
        inactiveImage: 'Images/mm_20_transparent_fade.png', // icon image for deselected marker type;
        isHidden: true // boolean - type is currently hidden
    };

};

function mousemove() {
    sitesListJustScrolled = false;
}

function loadSitesData(xml) {
    loadingMarkerFiles--;

    var markerNodes = xml.documentElement.getElementsByTagName("marker");
    for (var n = 0; n < markerNodes.length; n++) {
        var node = markerNodes[n];
        var site = node.getAttribute("site");

        markersData.push({
			lat: parseFloat(node.getAttribute("lat")),
			lng: parseFloat(node.getAttribute("lon")),
			name: node.getAttribute("name"),
			type: node.getAttribute("type"), // ['open', 'closed', 'microlight']
			href: (site != null && site != '') ? 'Sites/' + site + '.html' : null,
			description: node.firstChild.data
		});
    }

    var shapeNodes = xml.documentElement.getElementsByTagName("shape");
    for (n = 0; n < shapeNodes.length; n++) {
        createShapeFromNode(shapeNodes[n]);
    }

    markersLoaded();
}

function loadClubsData(csv) {
    loadingMarkerFiles--;

    var clubsDataRaw = CSVToArray(csv);

    for (var n = 0; n < clubsDataRaw.length; n++) {
        var thisClubData = clubsDataRaw[n];
        var club = {
            name: thisClubData[1],
            href: thisClubData[2],
            contactName: thisClubData[3],
            email: thisClubData[4],
            contactPhone: thisClubData[5],
            address: thisClubData[6],
            lat: parseFloat(thisClubData[7]),
            lng: parseFloat(thisClubData[8]),
            comments: thisClubData[9]
        };
        club.email = (club.email) ? "mailto:" + club.email : null;
        club.href = (club.href) ? "http://" + club.href : null;
        var description = club.contactName;
        if (club.email)
            description = "<a href='" + club.email + "'>" + description + "</a>";
        description = "<p>" + description + " " + club.contactPhone + "</p><p>" + club.address + "</p>";
        if (club.comments)
            description += "<p>" + club.comments + "</p>";

        markersData.push({
			lat: club.lat,
            lng: club.lng,
			name: club.name,
			type: 'club',
			href: club.href,
			description: description
		});
    }

    markersLoaded();
};

function loadSchoolsData(csv) {
    loadingMarkerFiles--;

    var schoolsDataRaw = CSVToArray(csv);

    for (var n = 0; n < schoolsDataRaw.length; n++) {
        var thisSchoolData = schoolsDataRaw[n];
        thisSchoolData.length = 15; // pad empty trailing fields
        var school = {
            name: thisSchoolData[1],
            href: thisSchoolData[2],
            instructorType: thisSchoolData[3],
            instructor: thisSchoolData[4],
            phone: thisSchoolData[5],
            email: thisSchoolData[6],
            address: thisSchoolData[7],
            lat: parseFloat(thisSchoolData[13]),
            lng: parseFloat(thisSchoolData[14]),
            isHG: (thisSchoolData[8] || "") != "",
            isHGM: (thisSchoolData[9] || "") != "",
            isPG: (thisSchoolData[10] || "") != "",
            isPGM: (thisSchoolData[11] || "") != "",
            isWM: (thisSchoolData[12] || "") != ""
        };
        school.email = (school.email) ? "mailto:" + school.email : null;
        school.href = (school.href) ? "http://" + school.href : null;
        var description = school.instructor;
        if (school.email)
            description = school.instructorType + ": <a href='" + school.email + "'>" + description + "</a>";
        var types = "";
        if (school.isHG) types += "<li>Hang Gliding</li>";
        if (school.isHGM) types += "<li>Motorised Hang Gliding</li>";
        if (school.isPG) types += "<li>Paragliding</li>";
        if (school.isPGM) types += "<li>Paramotoring</li>";
        if (school.isWM) types += "<li>Weightshift Microlighting</li>";
        description = "<ul>" + types + "</ul><p>" + description + " " + school.phone + "</p><p>" + school.address + "</p>";
        if (school.comments)
            description += "<p>" + school.comments + "</p>";

        markersData.push({
            lat: school.lat,
            lng: school.lng,
            name: school.name,
            type: 'school',
            href: school.href,
            description: description
        });
    }

    markersLoaded();
};

function markersLoaded() {
    if (loadingMarkerFiles) return; //still loading more data

    markersData.sort(function (a, b) { // sort by type, then by name
        if (typesSortOrder[a.type] < typesSortOrder[b.type]) return -1
        if (typesSortOrder[a.type] > typesSortOrder[b.type]) return 1

        var nameA = a.name.toLowerCase(), nameB = b.name.toLowerCase()
        if (nameA < nameB) return -1
        if (nameA > nameB) return 1
        return 0
    });

    for (var n = 0; n < markersData.length; n++)
        createMarker(markersData[n]);

    markersData = null;

    var initialLaunchIndex = markerIndexByName[urlParams.Pin];

    if (urlParams.Center || geocodeSuccess) {
        // the viewport has already been specified. If a launch has been specified, just pin it without panning
        if (initialLaunchIndex)
            setPin(initialLaunchIndex, false, true);
    }
    else if (initialLaunchIndex) {
        // center the map on the specified launch
        if (urlParams.Zoom)
            map.setZoom(urlParams.Zoom);
        else if (urlParams.Span)
            setMapCenterAndSpan(markers[initialLaunchIndex].Marker.position, urlParams.Span);
        else
            map.setZoom(12); // default zoom level for showing a launch

        setPin(initialLaunchIndex, true, true);
    }
    else {
        // there is no initial launch and no center, so we'll ignore any span or zoom level and just show all non-hidden markers
        var bounds = new google.maps.LatLngBounds;
        for (var i = 0; i < markers.length; i++) {
            if (!types[markers[i].Type].isHidden)
                bounds.extend(markers[i].Marker.position);
        }
        if (!bounds.isEmpty()) map.fitBounds(bounds);
    }

    setInfoPanel();
    setLaunchVisibilities();
}

function setInfoPanel() {
    var detail;
    if (hoveringMarker != null) {
        detail = markers[hoveringMarker].Description;
    }
    else if (hoveringShape) {
        detail = hoveringShape.description;
    }
    else if (pinnedMarker != null) {
        detail = markers[pinnedMarker].Description;
    }
    else {
        detail = detailIdle;
    }
    $("site-detail").innerHTML = detail;
}

function createShapeFromNode(node) {
    var shape;
    var category = node.getAttribute('category');
    var descriptionNodes = node.getElementsByTagName('desc');
    var shapeDescription;
    if (descriptionNodes.length != 0)
        shapeDescription = node.getElementsByTagName('desc')[0].firstChild.data;
    else
        shapeDescription = category;
    var coordSets = node.getElementsByTagName('coords');
    var polys = [];
    var shapeDim = shapeStyle[category].dimensions;
    var shapeColor = shapeStyle[category].color;

    if (shapeDim == 2) {
        for (var s = 0; s < coordSets.length; s++) {
            polys[s] = decodeLine(coordSets[s].firstChild.data);
        }

        shape = new google.maps.Polygon({
            map: map,
            paths: polys,
            fillColor: shapeColor,
            fillOpacity: .2,
            strokeColor: shapeColor,
            strokeOpacity: .4,
            strokeWeight: 1
        });
    }
    else if (shapeDim == 1) {
        shape = new google.maps.Polyline({
            map: map,
            path: decodeLine(coordSets[0].firstChild.data),
            strokeColor: shapeColor,
            strokeOpacity: .6,
            strokeWeight: 1
        });
    }

    shape.description = shapeDescription;
    google.maps.event.addListener(shape, 'mouseover', function () { hoveringShape = this; setInfoPanel() });
    google.maps.event.addListener(shape, 'mouseout', function () { hoveringShape = null; setInfoPanel() });

    return shape;
}

function createMarker(markerData) { //lat, lng, name, type, href, description
    var index = markers.length;
    var marker = new google.maps.Marker({
        icon: types[markerData.type].image,
        shadow: shadowImage,
        position: new google.maps.LatLng(markerData.lat, markerData.lng),
        map: map
    });

    google.maps.event.addListener(marker, 'mouseover', function () { hoveringMarker = index; markerHighlight(index, true); });
    google.maps.event.addListener(marker, 'mouseout', function () { hoveringMarker = null; markerDeHighlight(index); });
    google.maps.event.addListener(marker, 'click', function () { setPin(index, false, true) });
    if (markerData.href) google.maps.event.addListener(marker, 'dblclick', function () { openSitePage(); });

    var list = $('sites-list');
    var listOutOfRange = $('out-of-range');
    var li = document.createElement("li"); // <li> element for this marker on the markers list
    li.appendChild(document.createTextNode(markerData.name));
    li.style.backgroundImage = 'url(' + types[markerData.type].image + ')';
    var liOutOfRange = li.cloneNode(true);
    liOutOfRange.style.backgroundImage = 'url(' + types[markerData.type].fadeImage + ')';

    li.onmouseover = function () {
        if (sitesListJustScrolled)
            sitesListJustScrolled = false;
        else {
            hoveringMarker = index;
            markerHighlight(index, false)
        }
    };
    li.onmouseout = function () { markerDeHighlight(index) };
    li.onclick = function () { listClick(index) };
    liOutOfRange.onmouseover = li.onmouseover; // <li> element for this marker on the out-of-range list
    liOutOfRange.onmouseout = li.onmouseout;
    liOutOfRange.onclick = li.onclick;
    if (markerData.href != null) {
        li.ondblclick = function () { openSitePage() };
        liOutOfRange.ondblclick = li.ondblclick;
    };

    list.appendChild(li);
    listOutOfRange.appendChild(liOutOfRange);

    markerIndexByName[markerData.name] = index;

    launchDescription = "<h3>" + markerData.name + "</h3>" + markerData.description
    if (markerData.href) {
        if (markerData.type == 'school' || markerData.type == 'club')
            launchDescription += "<p>See the " + markerData.type + "'s <a href='" + markerData.href + "'>home page</a> for full details.</p>";
        else
            launchDescription += "<p>See <a href='" + markerData.href + "'>site page</a> for full details.</p>";
    }

    markers.push({
        "Marker": marker,
        "Type": markerData.type,
        "Description": launchDescription,
        "Href": markerData.href,
        "ListItem": li,
        "ListItemOutOfRange": liOutOfRange
    });
}

function listClick(index) {
    if (new Date().getTime() < doubleClickDeadline) { //double-click
        clearTimeout(clickTimeoutId); setPin(index, true, true); // handle first click immediately
        openSitePage();
    } else { //single-click or first click of a double-click
        setPin(index, false, false);
        clickTimeoutId = setTimeout('setPin("' + index + '", true, true)', doubleClickInterval);
        doubleClickDeadline = new Date().getTime() + doubleClickInterval;
    }
}

function mapMoved() {
    if (markers != null)
        setLaunchVisibilities();
    if (pinnedMarker != null)
        scrollIntoViewIfHidden(pinnedMarker);
    else
        $('sites-list-wrapper2').scrollTop = 0;
}

function resize() {
    var mapCenter;
    if (map != null) {
        mapCenter = map.getCenter();
    }

    //Width first, since width can influence height but not vice-versa
    var newWidth =
      document.body.clientWidth
    - $('sites-list-wrapper').offsetWidth
    - $('site-detail-wrapper').offsetWidth
    + 'px';

    $('wrapper-menu-top').style.width = newWidth;
    $('menu-top').style.width = newWidth;
    $('wrapper-content').style.width = newWidth;
    $('header').style.width = newWidth;

    var contentYOffset =
      $('header').offsetHeight
    + $('wrapper-menu-top').offsetHeight

    $('site-detail-wrapper').style.top = contentYOffset;
    $('sites-list-wrapper').style.top = contentYOffset;

    var totalHeight = document.body.parentNode.clientHeight;
    if (totalHeight == 0) {
        // IE6 or earlier
        totalHeight = document.body.clientHeight;
    }
    var contentHeight =
      totalHeight
    - contentYOffset
    - 1
    + 'px';
    $('sites-map').style.height = contentHeight;
    $('sites-list-wrapper').style.height = contentHeight;
    $('site-detail-wrapper').style.height = contentHeight;
    $('sites-list-wrapper2').style.height =
      totalHeight
    - contentYOffset
    - $('show-types').offsetHeight
    - 1
    + 'px';

    if (mapCenter != null) {
        map.setCenter(mapCenter);
    }

}

function markerHighlight(index, scroll) {
    markers[index].ListItem.className = 'highlight';
    markers[index].ListItemOutOfRange.className = 'highlight';
    if (scroll)
        scrollIntoViewIfHidden(index);
    //  hoveringMarker = index;
    setInfoPanel();
    var highlightImage = types[markers[index].Type].highlightImage;
    markers[index].Marker.setIcon(highlightImage);
    markers[index].Marker.setZIndex(maxZIndex);
    markers[index].ListItem.style.backgroundImage = 'url(' + highlightImage + ')';
    markers[index].ListItemOutOfRange.style.backgroundImage = 'url(' + highlightImage + ')';
}

function markerDeHighlight(index) {
    if (hoveringMarker == index)
        hoveringMarker = null;

    if (pinnedMarker != index) {
        if (pinnedMarker != null)
            markerHighlight(pinnedMarker, false);
        else if (hoveringMarker != null)
            markerHighlight(hoveringMarker, false);
        else
            setInfoPanel(); // to idle

        var marker = markers[index];
        marker.ListItem.className = '';
        marker.ListItemOutOfRange.className = '';
        var idleImage = types[marker.Type].image;
        marker.Marker.setIcon(idleImage);
        marker.Marker.setZIndex(undefined);
        marker.ListItem.style.backgroundImage = 'url(' + idleImage + ')';
        marker.ListItemOutOfRange.style.backgroundImage = 'url(' + types[marker.Type].fadeImage + ')';
    }
}

function clearPinned() {
    if (pinnedMarker != null) {
        var oldPinned = pinnedMarker;
        pinnedMarker = null;
        markerDeHighlight(oldPinned);
    }
}

function setPin(index, pan, scroll) {
    sitesListJustScrolled = false;
    if (pinnedMarker != null && pinnedMarker != index)
        clearPinned();
    pinnedMarker = index;
    markerHighlight(index, false);
    if (pan) {
        sitesListJustScrolled = true;
        map.panTo(markers[index].Marker.position);
        setLaunchVisibilities();
    }
    if (scroll)
        scrollIntoViewIfHidden(index);
}

function scrollIntoViewIfHidden(index) {
    container = $('sites-list-wrapper2');
    var visTop = container.scrollTop;
    var visBottom = visTop + container.offsetHeight;
    var li;
    if (markers[index].ListItem.style.display != 'none')
        li = markers[index].ListItem;
    else
        li = markers[index].ListItemOutOfRange;
    var top = li.offsetTop;
    var bottom = top + li.offsetHeight;
    if (visTop > top || visBottom < bottom)
        container.scrollTop = (top + bottom - container.offsetHeight) / 2;
}

function openSitePage() {
    //      var body = document.getElementsByTagName("body")[0];
    //      window.getSelection().collapse(body);                 // removed, since doesn't work in ie

    if (markers[pinnedMarker].Href != null && new Date().getTime() > openSitePageCooldown) {
        openSitePageCooldown = new Date().getTime() + 2 * doubleClickInterval; // it's safe to be generous with cooldown time
        window.open(markers[pinnedMarker].Href, '_blank'); // new window
        //    window.location = markers[pinnedMarker].Href; // same window
    }
}

function toggleTypeVis(type) {
    types[type].isHidden = !types[type].isHidden;

    if (types[type].isHidden) {
        if (pinnedMarker != null)
            if (markers[pinnedMarker].Type == type)
                clearPinned();
        if (hoveringMarker != null)
            if (markers[hoveringMarker].Type == type)
                markerDeHighlight(hoveringMarker);
    }

    setTypeHeaderImage(type);
    if (type == 'car166')
        showHideCAR166();
    else
        setLaunchVisibilities();
}

function setTypeHeaderImage(type) {
    if (types[type].isHidden)
        $(type).style.backgroundImage = 'url(' + types[type].inactiveImage + ')'
    else
        $(type).style.backgroundImage = 'url(' + types[type].image + ')';
}

function setLaunchVisibilities() {
    var visBounds = map.getBounds();
    if (!visBounds) return;

    var anyVisLaunches = false;
    var anyOutOfRangeLaunches = false;

    for (i = 0; i < markers.length; i++) {
        var marker = markers[i];

        if (types[marker.Type].isHidden) {
            marker.ListItem.style.display = 'none';
            marker.ListItemOutOfRange.style.display = 'none';
            marker.Marker.setVisible(false);
        } else {
            if (visBounds.contains(marker.Marker.getPosition())) {
                marker.ListItem.style.display = 'block';
                marker.ListItemOutOfRange.style.display = 'none';
                anyVisLaunches = true;
            } else {
                marker.ListItem.style.display = 'none';
                marker.ListItemOutOfRange.style.display = 'block';
                anyOutOfRangeLaunches = true;
            }
            marker.Marker.setVisible(true);
        }
    }

    sitesListJustScrolled = true;

    if (anyVisLaunches && anyOutOfRangeLaunches)
        $('sites-list').style.borderBottom = 'solid 1px #666';
    else
        $('sites-list').style.borderBottom = 'none';
}

function showHideCAR166() {
    if (types["car166"].isHidden) {
        if (aerodromes != undefined)
            for (var n = 0; n < aerodromes.length; n++) {
                aerodrome = aerodromes[n];
                aerodrome.circle.setMap(null);
                aerodrome.circle = null;
                aerodrome.Label.setMap(null);
                aerodrome.Label = null;
            }
    }
    else {
        if (aerodromes == undefined)
            downloadUrl("VHFAerodromes.json", function (request) { loadCAR166(request.responseText) });
        else
            showCAR166();
    }
}

function loadCAR166(json) {
    aerodromes = eval("(" + json + ")");

    var aerodrome;
    for (var n = 0; n < aerodromes.length; n++) {
        aerodrome = aerodromes[n];
        aerodrome.Code = "Y" + aerodrome.Code;
        aerodrome.position = new google.maps.LatLng(aerodrome.Lat, aerodrome.Lng);
    }
    showHideCAR166();
}

function showCAR166() {
    var aerodrome;

    for (var n = 0; n < aerodromes.length; n++) {
        aerodrome = aerodromes[n];

        aerodrome.circle = new google.maps.Circle({
            center: aerodrome.position,
            fillColor: "#E56717",
            fillOpacity: .3,
            strokeWeight: 0.01,
            radius: 18520, // 10 nautical miles
            map: map
        });
        aerodrome.circle.aerodrome = aerodrome;
        aerodrome.Label = new LabelOverlay(aerodrome);

        google.maps.event.addListener(aerodrome.circle, 'mouseover', function () {
            highlightAerodrome(this.aerodrome, true)
        });
        google.maps.event.addListener(aerodrome.circle, 'mouseout', function () {
            highlightAerodrome(this.aerodrome, false)
        });
        google.maps.event.addListener(aerodrome.circle, 'dblclick', function () {
            window.open("http://www.airservicesaustralia.com/aip/current/ersa/FAC_" + this.aerodrome.Code + "_25-Aug-2011.pdf", '_blank'); // new window
        });
    }
}

function highlightAerodrome(aerodrome, highlight) {
    aerodrome.circle.setOptions({
        strokeWeight: (highlight) ? 1 : 0.01
    });
    aerodrome.Label.div_.style.fontWeight = (highlight) ? 'bold' : '';
    aerodrome.Label.positionDiv();
}

LabelOverlay.prototype = new google.maps.OverlayView();
function LabelOverlay(aerodrome) {
    if (!(this instanceof arguments.callee))
        throw new Error("Constructor called as a function.");

    this.aerodrome = aerodrome;
    this.setMap(map);
}

LabelOverlay.prototype.onAdd = function () {
    var div = document.createElement("div");
    div.style.position = "absolute";
    div.style.textAlign = "center";
    div.appendChild(document.createTextNode(this.aerodrome.Name));

    this.div_ = div;

    this.getPanes().overlayLayer.appendChild(div);
}

LabelOverlay.prototype.draw = function () {
    this.div_.style.fontSize = Math.pow(1.25, map.getZoom() + 4) + 'px';
    this.positionDiv();
}

LabelOverlay.prototype.positionDiv = function () {
    var point = this.getProjection().fromLatLngToDivPixel(this.aerodrome.position);
    var div = this.div_;
    div.style.left = point.x - div.clientWidth / 2 + 'px';
    div.style.top = point.y - div.clientHeight / 2 + 'px';
}

LabelOverlay.prototype.onRemove = function () {
    this.div_.parentNode.removeChild(this.div_);
    this.div_ = null;
}

// ---- utility functions ----

function getUrlParams() {
    // url parameters, first three are equivalent to Google Maps params:
    var llParam = urlParam('ll'); // lat, lon
    var zParam = urlParam('z');  // zoom
    var spnParam = urlParam('spn'); // span - lat, lon
    var pinParam = urlParam('pin'); // pinned launch name
    var typeParam = urlParam('type'); // shown types (e.g. 'open,closed')
    var qParam = urlParam('q'); // query - name of area to be shown via geocoding

    var center;
    if (llParam) {
        var coords = llParam.split(",");
        center = new google.maps.LatLng(coords[0], coords[1]);
    };

    var zoom;
    if (zParam) {
        zoom = parseFloat(zParam);
        if (isNaN(zoom)) zoom = null;
    }

    var span;
    if (spnParam) {
        coords = spnParam.split(",");
        span = new google.maps.LatLng(coords[0], coords[1]);
    };

    var types;
    if (typeParam)
        types = typeParam.split(",");

    return {
        "Center": center,
        "Zoom": zoom,
        "Span": span,
        "Query": qParam,
        "Pin": pinParam,
        "Types": types
    }
}

function urlParam(name) {
    name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
    var regexS = "[\\?&]" + name + "=([^&#]*)";
    var regex = new RegExp(regexS);
    var results = regex.exec(window.location.href);
    if (results == null)
        return null;
    else
        return decodeURI(results[1]);
}

function setMapCenterAndSpan(center, span) {
    map.fitBounds(new google.maps.LatLngBounds(
                new google.maps.LatLng(center.lat() - span.lat() / 2, center.lng() - span.lng() / 2),
                new google.maps.LatLng(center.lat() + span.lat() / 2, center.lng() + span.lng() / 2)
        ));
}

//function saveState() {
//  window.location.hash = getState();
//}

//function getState() {
//  var center = map.getCenter();
//  return("ll=" + center.lat() + "," + center.lng());
//}

//---------------------------------------------------------------
/**
* This functions wraps XMLHttpRequest open/send function.
* It lets you specify a URL and will call the callback if
* it gets a status code of 200.
* @param {String} url The URL to retrieve
* @param {Function} callback The function to call once retrieved.
*/
function downloadUrl(url, callback) {
    var status = -1;
    var request = createXmlHttpRequest();
    if (!request) {
        return false;
    }

    request.onreadystatechange = function () {
        if (request.readyState == 4) {
            try {
                status = request.status;
            } catch (e) {
                // Usually indicates request timed out in FF.
            }
            if (status == 200 || status == 0) { //TODO - should be 200, getting 0
                callback(request);
                request.onreadystatechange = function () { };
            }
        }
    }
    request.open('GET', url, true);
    try {
        request.send(null);
    } catch (e) {
        changeStatus(e);
    }
};

/**
* Returns an XMLHttp instance to use for asynchronous
* downloading. This method will never throw an exception, but will
* return NULL if the browser does not support XmlHttp for any reason.
* @return {XMLHttpRequest|Null}
*/
function createXmlHttpRequest() {
    try {
        if (typeof ActiveXObject != 'undefined') {
            return new ActiveXObject('Microsoft.XMLHTTP');
        } else if (window["XMLHttpRequest"]) {
            return new XMLHttpRequest();
        }
    } catch (e) {
        changeStatus(e);
    }
    return null;
};

// This will parse a delimited string into an array of
// arrays. The default delimiter is the comma, but this
// can be overriden in the second argument.
function CSVToArray(strData, strDelimiter) {
    strDelimiter = (strDelimiter || ",");

    var objPattern = new RegExp(
    (
    // Delimiters.
        "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +

    // Quoted fields.
        "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +

    // Standard fields.
        "([^\"\\" + strDelimiter + "\\r\\n]*))"
        ),
        "gi"
    );

    var arrData = [[]];
    var arrMatches = null;

    // Keep looping over the regular expression matches
    // until we can no longer find a match.
    while (arrMatches = objPattern.exec(strData)) {

        // Get the delimiter that was found.
        var strMatchedDelimiter = arrMatches[1];

        // Check to see if the given delimiter has a length
        // (is not the start of string) and if it matches
        // field delimiter. If id does not, then we know
        // that this delimiter is a row delimiter.
        if (
            strMatchedDelimiter.length &&
            (strMatchedDelimiter != strDelimiter)
        ) {
            // Since we have reached a new row of data,
            // add an empty row to our data array.
            arrData.push([]);
        }

        if (arrMatches[2]) {
            // We found a quoted value. When we capture
            // this value, unescape any double quotes.
            var strMatchedValue = arrMatches[2].replace(
                new RegExp("\"\"", "g"),
                "\""
            );
        } else {
            // We found a non-quoted value.
            var strMatchedValue = arrMatches[3];
        }

        // Add value string to the data array.
        arrData[arrData.length - 1].push(strMatchedValue);
    }

    //discard header row
    arrData.splice(0, 1);
    return (arrData);
};

// Decode an encoded polyline string into an array of coordinates.
// Adapted from http://code.google.com/apis/maps/documentation/utilities/include/polyline.js
function decodeLine(encoded) {
    var len = encoded.length;
    var index = 0;
    var array = [];
    var lat = 0;
    var lng = 0;

    while (index < len) {
        var b;
        var shift = 0;
        var result = 0;
        do {
            b = encoded.charCodeAt(index++) - 63;
            result |= (b & 0x1f) << shift;
            shift += 5;
        } while (b >= 0x20);
        var dlat = ((result & 1) ? ~(result >> 1) : (result >> 1));
        lat += dlat;

        shift = 0;
        result = 0;
        do {
            b = encoded.charCodeAt(index++) - 63;
            result |= (b & 0x1f) << shift;
            shift += 5;
        } while (b >= 0x20);
        var dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
        lng += dlng;

        array.push(new google.maps.LatLng(lat * 1e-5, lng * 1e-5));
    }

    return array;
}

function $(ID) { // "Poor man's JQuery". Thanks Leon http://secretgeek.net/higgins/slides_alt_net.html#44
    return document.getElementById(ID);
}
