﻿



/*
Script to support Andornot mapping UI.
Peter Tyrrell, Andornot 2009-2010
*/

(function () {

    var andornotMap = window.andornotMap = window.$map = {};

    // general map settings
    $map.settings = {
        map: '',
        sectors: 6,
        columns: 9,
        ie6: {
            shim: "/chartingchange/layout/images/png_fix.gif"
        },
        serviceUrl: "/chartingchange/services/inmagic.asmx/GetPointsOfInterest",
        viewport: {
            maxWidth: 1650,
            minWidth: 200,
            maxHeight: 1500,
            minHeight: 200
        },
        dragArea: {
            maxWidth: 4850,
            minWidth: 1750,
            minMarginLeft: -1700,
            maxHeight: 6100,
            minHeight: 1600,
            minMarginTop: -1550,
            buffer: 500
        },
        draggable: {
            maxMarginLeft: 1700,
            maxMarginTop: 1550
        },
        zoom: {
            current: 100,
            increments: [25, 50, 75, 100, 125, 150, 175],
            levels: [{
                increment: 25,
                positions: []
            },
                {
                    increment: 50,
                    positions: []
                },
                {
                    increment: 75,
                    positions: []
                },
                {
                    increment: 100,
                    positions: []
                },
                {
                    increment: 125,
                    positions: []
                },
                {
                    increment: 150,
                    positions: []
                },
                {
                    increment: 175,
                    positions: []
                }]
        }
    };

    $map.utils = function () {
        var _tmplCache = {};
        var parseTemplate = function (str, data) {
            /// <summary>
            /// Client side template parser that uses &lt;#= #&gt; and &lt;# code #&gt; expressions.
            /// and # # code blocks for template expansion.
            /// NOTE: chokes on single quotes in the document in some situations
            ///       use &amp;rsquo; for literals in text and avoid any single quote
            ///       attribute delimiters.
            /// </summary>    
            /// <param name="str" type="string">The text of the template to expand</param>    
            /// <param name="data" type="var">
            /// Any data that is to be merged. Pass an object and
            /// that object's properties are visible as variables.
            /// </param>    
            /// <returns type="string" />  
            var err = "";
            try {
                var func = _tmplCache[str];
                if (!func) {
                    var strFunc =
                "var p=[],print=function(){p.push.apply(p,arguments);};" +
                            "with(obj){p.push('" +
                str.replace(/[\r\t\n]/g, " ")
                   .replace(/'(?=[^#]*#>)/g, "\t")
                   .split("'").join("\\'")
                   .split("\t").join("'")
                   .replace(/<#=(.+?)#>/g, "',$1,'")
                   .split("<#").join("');")
                   .split("#>").join("p.push('")
                   + "');}return p.join('');";

                    func = new Function("obj", strFunc);
                    _tmplCache[str] = func;
                }
                return func(data);
            } catch (e) { err = e.message; }
            return "< # ERROR: " + err + " # >";
        };
        var getNumberOrLimit = function (number, minNumber, maxNumber) {
            if (number < minNumber) {
                return minNumber;
            }
            if (number > maxNumber) {
                return maxNumber;
            }
            return number;
        };
        var valueOrLimit = function (i, array) {
            if (i > array.length - 1) {
                return array[array.length - 1];
            }
            else if (i < 0) {
                return array[0];
            }
            return array[i];
        };
        // sets common animated show/hide behaviour for an item to display and its trigger
        var setOnOff = function (trigger, eventOn, displayItem, eventOff, autoOff) {

            var fade = function () {
                displayItem.fadeOut(1000);
            };

            trigger.bind(eventOn, function () {
                displayItem
                .stop(true, true)
                .show(1, function () {
                    // optional
                    if (autoOff === true) {
                        $(this).fadeOut(3000);
                    }
                })
                .mouseover(function () {
                    $(this).stop(true, true).fadeIn(0);
                })
                .mouseout(fade);
            });

            // optional
            if (eventOff !== null) {
                trigger.bind(eventOff, fade);
            }
        };



        return {
            getNumberOrLimit: function (number, minNumber, maxNumber) {
                return getNumberOrLimit(number, minNumber, maxNumber);
            },
            valueOrLimit: function (i, array) {
                return valueOrLimit(i, array);
            },
            parseTemplate: function (str, data) {
                return parseTemplate(str, data);
            },
            setOnOff: function (trigger, eventOn, displayItem, eventOff, autoOff) {
                setOnOff(trigger, eventOn, displayItem, eventOff, autoOff);
            }
        };
    } ();

    // dialog with point of interest info
    $map.dialog = function () {
        var bind = function (result) {
            var html = $map.utils.parseTemplate($map.$dialogTemplate.html(), { result: result });
            $map.$dialog
                .html(html)
                .mouseover(function (e) {
                    // stop any animation fx
                    $(this).stop(true, true).fadeIn(0);
                })
                .data("pin", result);
            /*.mouseout(function (e) {
            $map.dialog.hide(500);
            })*/

            // ie6 png fix
            $map.$dialog.supersleight({ shim: $map.settings.ie6.shim, imgs: false });


        };
        var hide = function (speed) {
            $map.$dialog.fadeOut((typeof speed === "undefined" || speed === null) ? 300 : speed);
            // de-highlight associated pins
            var point = $(this).data("pin").PointNumber;
            $("#pin_" + point + ", #gutter_pin_" + point).removeClass("pointer_hover");

        };
        var show = function (top, left, parent, speed) {
            $map.$dialog
                .css({
                    'top': top,
                    'left': left
                })
                .stop(true, true)
                .fadeIn((typeof speed === "undefined" || speed === null) ? 500 : speed);

            if (parent) {
                $map.$dialog.appendTo(parent);
            }

            // todo: snap to viewport if obscured? see jquery.ui position collision
        };

        return {
            bind: function (result) {
                bind(result);
            },
            hide: function (speed) {
                hide(speed);
            },
            show: function (top, left, parent, speed) {
                show(top, left, parent, speed);
            }
        };
    } ();

    $map.map = function () {
        var init = function () {
            // create commonly used jquery objects
            $map.$dialog = $("#dialog");
            $map.$dialogTemplate = $("#dialog_template");
            $map.$viewport = $("#map_viewport");
            $map.$dragArea = $("#map_drag_area");
            $map.$draggable = $("#map_draggable");
            $map.$wrapper = $("#map_wrapper");
            $map.$mapLeft = $("#map_left");
            $map.$mapRight = $("#map_right");
            $map.$mapGutter = $("#map_gutter");
            $map.$mapHeader = $("#map_header");
            $map.$pinTemplate = $("#pin_template");


            // force map resize when window is resized
            $(window).resize(function (e) {
                resize();
            });

            // set initial map size
            resize();

            // set 'no zoom' warning on mousewheel
            //$map.utils.setOnOff($map.$viewport, "mousewheel", $("#zoom_msg"), null, true); NOTE: needed if no zoom

            // hack for nested draggable IE bug in jQuery UI
            $.extend($.ui.draggable.prototype, (function (orig) {
                return {
                    _mouseCapture: function (event) {
                        var result = orig.call(this, event);
                        if (result && $.browser.msie) event.stopPropagation();
                        return result;
                    }
                };
            })($.ui.draggable.prototype["_mouseCapture"]));


            // set map draggable behaviour
            $map.$draggable.draggable({
                containment: 'parent',
                cursor: 'move',
                stop: revertMargin
            });

            // set dialog draggable behaviour
            $map.$dialog.draggable({
                snap: "#map_viewport",
                snapTolerance: 40
            });

            // set map tile lazy load
            $("div.lazyload img").lazyload({
                effect: "show",
                container: $("#map_viewport")
            });

            // ie6 png fix
            $("#zoom").supersleight({ shim: $map.settings.ie6.shim, imgs: true });

        };
        var resize = function (multiplier) {

            /*
            dragArea width = (draggable * 2) - viewport + (buffer * 2)
            dragArea marginLeft = -(wrapper - viewport + buffer)
            draggable marginLeft = +(wrapper - viewport + buffer)
            */

            // set viewport width
            var viewportWidth = $map.utils.getNumberOrLimit($(window).width() - $map.$mapLeft.width(), $map.settings.viewport.minWidth, $map.settings.viewport.maxWidth);
            $map.$viewport.width(viewportWidth);

            // set viewport height
            var viewportHeight = $map.utils.getNumberOrLimit($(window).height() - $map.$mapHeader.height(), $map.settings.viewport.minHeight, $map.settings.viewport.maxHeight);
            $map.$viewport.height(viewportHeight);
            $map.$mapLeft.height(viewportHeight);

            // apply width multiplier
            if (multiplier !== undefined) {
                $map.$draggable.width($map.$draggable.width() * multiplier);
            }
            // calculate remaining map widths
            $map.$dragArea
                    .width(function () {
                        var width = ($map.$draggable.width() * 2) - viewportWidth + ($map.settings.dragArea.buffer * 2);
                        return $map.utils.getNumberOrLimit(width, $map.$draggable.width() + ($map.settings.dragArea.buffer * 2), width);

                    } ())
                    .css("margin-left", function () {
                        // drag_area margin-left is always negative
                        var x = $map.$draggable.width() - viewportWidth + $map.settings.dragArea.buffer;
                        return ((x > 0 ? x : $map.settings.dragArea.buffer) * -1) + "px";
                    });
            $map.$draggable
                    .css({ 'margin-left': function () {
                        // draggable margin-left is always positive
                        var x = $map.$draggable.width() - viewportWidth + $map.settings.dragArea.buffer;
                        return (x > 0 ? x : $map.settings.dragArea.buffer) + "px";
                    },
                        "left": function () {
                            // draggable left is always negative
                            var x = $map.$draggable.width() - viewportWidth + $map.settings.dragArea.buffer;
                            return ((x > 0 ? x : $map.settings.dragArea.buffer) * -1) + "px";
                        }
                    });

            // apply height multiplier
            if (multiplier !== undefined) {
                $map.$draggable.height($map.$draggable.height() * multiplier);
            }
            // calculate remaining map heights
            $map.$dragArea
                        .height(function () {
                            var height = ($map.$draggable.height() * 2) - viewportHeight + ($map.settings.dragArea.buffer * 2);
                            return $map.utils.getNumberOrLimit(height, $map.$draggable.height() + ($map.settings.dragArea.buffer * 2), height);
                        } ())
                        .css("margin-top", function () {
                            // drag_area margin-top always negative
                            var y = $map.$draggable.height() - viewportHeight + $map.settings.dragArea.buffer;
                            return ((y > 0 ? y : $map.settings.dragArea.buffer) * -1) + "px";
                        });
            $map.$draggable
                        .css({ "margin-top": function () {
                            // draggable margin-top always positive
                            var y = $map.$draggable.height() - viewportHeight + $map.settings.dragArea.buffer;
                            return (y > 0 ? y : $map.settings.dragArea.buffer) + "px";
                        },
                            "top": function () {
                                // draggable top always negative
                                var y = $map.$draggable.height() - viewportHeight + $map.settings.dragArea.buffer;
                                return ((y > 0 ? y : $map.settings.dragArea.buffer) * -1) + "px";
                            }
                        });


            if ($map.$draggable.width() < $map.$viewport.width()) {
                $map.$viewport.width($map.$draggable.width());
            }
            if ($map.$draggable.height() < $map.$viewport.height()) {
                $map.$viewport.width($map.$draggable.height());
            }

        };

        // if map pulled away from edge, revert back
        var revertMargin = function (options) {

            var opts = $.extend({}, $map.map.revertMargin.defaults, options);

            var defaults = {
                speed: 300
            };

            var x = $map.$draggable.offset().left - $map.$viewport.offset().left;
            var y = $map.$draggable.offset().top - $map.$viewport.offset().top;
            var animation = { animate: false };
            var xBuffer = $map.$dragArea.width() - $map.$draggable.outerWidth(true);
            var yBuffer = $map.$dragArea.height() - $map.$draggable.outerHeight(true);
            var dragLeft = parseInt($map.$dragArea.css("margin-left"));
            var dragTop = parseInt($map.$dragArea.css("margin-top"));

            // left margin showing
            if (x > 0) {
                animation.left = dragLeft + "px";
                animation.animate = true;
            }
            // right margin showing
            else if (x < (dragLeft + xBuffer)) {
                animation.left = (dragLeft * 2 + xBuffer) + "px";
                animation.animate = true;
            }
            // top margin showing
            if (y > 0) {
                animation.top = dragTop + "px";
                animation.animate = true;
            }
            // bottom margin showing
            else if (y < (dragTop + yBuffer)) {
                animation.top = (dragTop * 2 + yBuffer) + "px";
                animation.animate = true;
            }

            if (animation.animate === true) {
                $map.$draggable.animate(animation, opts.speed);
            }

            // load any tiles that have appeared in the viewport
            lazyload();
        };

        var focusPin = function (pinOffsetTop, pinOffsetLeft, duration, callback) {

            var animation = { animate: false };

            var ptop = pinOffsetTop;
            var vtop = $map.$viewport.offset().top + 100;
            if (ptop < vtop) {
                // pin needs to come down
                animation.top = "+=" + (vtop - ptop) + "px";
                animation.animate = true;
            }
            else if (ptop > vtop) {
                // pin needs to come up
                animation.top = "-=" + (ptop - vtop) + "px";
                animation.animate = true;
            }

            var pleft = pinOffsetLeft;
            var vleft = $map.$viewport.offset().left + 100;
            if (pleft < vleft) {
                // pin needs to come right
                animation.left = "+=" + (vleft - pleft) + "px";
                animation.animate = true;
            }
            else if (pleft > vleft) {
                // pin needs to come left
                animation.left = $map.$draggable.position().left - (pleft - vleft) + "px";
                animation.animate = true;
            }

            if (animation.animate === true) {
                $map.$draggable.animate(animation, duration, "swing", callback);
            }
            else {
                callback();
            }
        };

        var lazyload = function () {
            var settings = { container: $("#map_viewport"), threshold: 0 };
            $("div.lazyload img").each(function () {
                if ($(this).attr("src")) { return; }
                if (!$.belowthefold(this, settings) && !$.rightoffold(this, settings) && !$.abovethetop(this, settings) && !$.leftofbegin(this, settings)) {
                    $(this).trigger("appear");
                }
            });
        };

        var reposition = function (offsets, callback) {
            // adjust map position

            var top = parseFloat($map.$draggable.css("top"));
            var left = parseFloat($map.$draggable.css("left"));

            var topAdd = offsets.dragOffset.top + offsets.zoomOffset.top;
            var leftAdd = offsets.dragOffset.left + offsets.zoomOffset.left;

            $map.$draggable.css({
                "top": (top - topAdd).toString() + "px",
                "left": (left - leftAdd).toString() + "px"
            });

            if (callback !== undefined) {
                callback();
            }
        };



        var offsets = function (multiplier) {

            // get map coordinates at viewport center as {x:n, y:n} 
            var getViewportCenter = function () {
                var centerY = $map.$viewport.offset().top - $map.$draggable.offset().top + ($map.$viewport.height() * .5);
                var centerX = $map.$viewport.offset().left - $map.$draggable.offset().left + ($map.$viewport.width() * .5);
                return { x: centerX, y: centerY };
            };

            // measures map offset from default coordinates 0,0 as {top:n, left:n}
            var getDragOffset = function () {
                var top = $map.$viewport.offset().top - $map.$draggable.offset().top;
                var left = $map.$viewport.offset().left - $map.$draggable.offset().left;
                return { top: top, left: left };
            };

            // measures map offset due to magnification as {top:n, left:n}j
            var getZoomOffset = function (multiplier) {
                var center = getViewportCenter();
                var top = multiplier === undefined ? 0 : ((center.y * multiplier) - center.y);
                var left = multiplier === undefined ? 0 : ((center.x * multiplier) - center.x);
                return { top: top, left: left };
            };

            return {
                dragOffset: getDragOffset(),
                zoomOffset: getZoomOffset(multiplier)
            };
        };

        var zoom = function (current, target) {



            var multiplier = target / current;

            // get drag offset and current centerpoint before any changes
            var offsets = $map.map.offsets(multiplier);

            // special case, revert to original 100% numbers
            if (target === 100) {

                // revert map width/height
                $("#map_draggable, #map_wrapper, div.map_tiles img").width("").height("");
                $map.map.resize();
                $map.tiles.size();
                $map.map.reposition(offsets);
                return $map.pins.reposition($("#map_wrapper span.pointer"), 1);
            }

            $map.map.resize(multiplier);

            // calc widths, heights with minimum of processing
            var defaultWidth = 0, defaultHeight = 0;
            var $tiles = $("div.map_tiles img");
            $tiles.each(function (i, el) {

                var $el = $(el);

                var width = 0, height = 0;
                // todo: do both or either selectors have to satisfy?
                if ($el.is(".thin, .short") === true) {
                    width = Math.round($el.width() * multiplier);
                    height = Math.round($el.height() * multiplier);
                    //console.log("thin/short w = %d, h = %d", width, height);
                }
                else {
                    if (defaultWidth === 0) {
                        defaultWidth = Math.round($el.width() * multiplier);
                        defaultHeight = Math.round($el.height() * multiplier);
                        width = defaultWidth;
                        height = defaultHeight;
                        //console.log("defaultWidth is 0, w = %d, h = %d", width, height);
                    }
                    else {
                        width = defaultWidth;
                        height = defaultHeight;
                        //console.log("normal case, w = %d, h = %d", width, height);
                    }
                }
                $el.css({ "width": width + "px", "height": height + "px" });


            });

            // let map wrapper take width from one row of content
            var w = 0;
            $("div.map_tiles").children(":lt(" + $map.settings.columns + ")").each(function (i, el) {
                w += $(el).width();
                //console.log("%d +", w);
            });
            $("#map_wrapper").width(w);
            //console.log("= %d", w);


            // if map smaller than viewport, do not offset
            if ($map.$viewport.width() < $map.$draggable.width()) {
                $map.map.reposition(offsets);
            }

            // get stored pin positions for target, if any
            var level = $.grep($map.settings.zoom.levels, function (n, i) {
                return (n.increment === target);
            })[0];

            // pass reposition the array of pin positions, return same or calculated positions
            return $map.pins.reposition($("#map_wrapper span.pointer"), multiplier, level.positions);
        };


        var incrementZoom = function (increment) {
            var zoomIndex = $.inArray($map.settings.zoom.current, $map.settings.zoom.increments);
            if (zoomIndex === -1) { return; }
            var zoomTo = $map.utils.valueOrLimit(zoomIndex + increment, $map.settings.zoom.increments);
            if (zoomTo === $map.settings.zoom.current) {
                return;
            }

            // prezoom
            $map.$dialog.hide();
            
            $("#viewport").animate({ opacity: 0.9 });
            

            $map.settings.zoom.levels[zoomIndex + increment].positions = zoom($map.settings.zoom.current, zoomTo);
            $map.settings.zoom.current = zoomTo;

            // postzoom
            // todo: end animate
            $("#viewport").animate({ opacity: 0.2 });
        };

        return {
            init: function () {
                init();
            },
            resize: function (multiplier) {
                resize(multiplier);
            },
            revertMargin: function (options) {
                revertMargin(options);
            },
            focusPin: function (pinOffsetTop, pinOffsetLeft, duration, callback) {
                focusPin(pinOffsetTop, pinOffsetLeft, duration, callback);
            },
            lazyload: function () {
                lazyload();
            },
            reposition: function (offsets) {
                reposition(offsets);
            },
            offsets: function (multiplier) {
                return offsets(multiplier);
            },
            zoom: function (current, target) {
                return zoom(current, target);
            },
            incrementZoom: function (increment) {
                incrementZoom(increment);
            }
        };
    } ();

    $map.nav = function () {
        var init = function () {

            var setNavItem = function (trigger, displayItem) {
                $map.utils.setOnOff(trigger, "click", displayItem, "mouseout");
            };

            setNavItem($("#map_choices_button"), $("#map_choices"));
            setNavItem($("#map_help_button"), $("#map_help"));
            // 'no zoom' warning
            //setNavItem($("#zoom"), $("#zoom_msg"));
        };

        return {
            init: function () { init(); }
        };
    } ();

    $map.compass = {
        init: function () {

            var duration = 75;
            var scroll = "25px";

            // scroll map recursively on mousedown
            $("#top_left").mousedown(function () {
                $map.$draggable.animate({ top: "+=" + scroll, left: "+=" + scroll }, duration, "linear", arguments.callee);
            });
            $("#top_center").mousedown(function () {
                $map.$draggable.animate({ top: "+=" + scroll }, duration, "linear", arguments.callee);
            });
            $("#top_right").mousedown(function () {
                $map.$draggable.animate({ top: "+=" + scroll, left: "-=" + scroll }, duration, "linear", arguments.callee);
            });
            $("#mid_left").mousedown(function () {
                $map.$draggable.animate({ left: "+=" + scroll }, duration, "linear", arguments.callee);
            });
            $("#mid_right").mousedown(function () {
                $map.$draggable.animate({ left: "-=" + scroll }, duration, "linear", arguments.callee);
            });
            $("#btm_left").mousedown(function () {
                $map.$draggable.animate({ top: "-=" + scroll, left: "+=" + scroll }, duration, "linear", arguments.callee);
            });
            $("#btm_center").mousedown(function () {
                $map.$draggable.animate({ top: "-=" + scroll }, duration, "linear", arguments.callee);
            });
            $("#btm_right").mousedown(function () {
                $map.$draggable.animate({ top: "-=" + scroll, left: "-=" + scroll }, duration, "linear", arguments.callee);
            });

            $("div.compass div").mouseup(function () {
                $map.$draggable.stop(true);
                $map.map.revertMargin();
            });
        }
    };
    $map.blurb = function () {
        return {
            init: function () {
                $map.$blurb = $("#blurb");
                $map.$toggle = $("#blurb_toggle");

                var blurbHeight = (parseInt($map.$blurb.css("line-height")) * 3);
                if (blurbHeight === "NaN") {
                    blurbHeight = 50;
                }
                $map.$blurb.css("height", blurbHeight + "px");

                $map.$toggle.toggle(
                    function (e) {
                        e.preventDefault();
                        this.blur();
                        $map.$blurb.css({ 'height': 'auto' });
                        $(this).html("less...");
                    },
                    function (e) {
                        e.preventDefault();
                        this.blur();
                        $map.$blurb.css({ 'height': blurbHeight });
                        $(this).html("more...");
                    }
                );
            }
        };
    } ();

    $map.tiles = function () {
        var size = function (width, height) {

            var w, h;
            if (width === undefined || width === null) {
                w = "";
            }
            if (height === undefined || height === null) {
                h = "";
            }
            else {
                w = width + "px";
                h = height + "px";
            }

            var $tiles = $("div.map_tiles img");
            $tiles.each(function (i, el) {
                $(this)
                    .width(w)
                    .height(h)
            });
        };

        return {
            size: function (width, height) { return size(width, height); }
        };
    } ();

    $map.pins = function () {
        var init = function () {

            var dialogTimeoutId = [];

            // set overall map pin behaviour at template
            $map.$pinTemplate
                    .click(function (e) {
                        // show dialog
                        e.preventDefault();

                        var pin = $(this);
                        var pinData = pin.data("result");
                        var $mapPin = $("#pin_" + pinData.PointNumber);

                        // scroll map to center associated pin
                        $map.map.focusPin($mapPin.offset().top, $mapPin.offset().left, 500, function () {
                            // bind and show dialog with pin data
                            $map.dialog.bind(pinData);
                            $map.dialog.show($mapPin.position().top - 25, $mapPin.position().left + $mapPin.width() + 5, $mapPin.offsetParent());
                            $map.map.lazyload();
                        });
                    })
                    .hover(
                        function () {

                            var $pin = $(this);

                            // highlight associated gutter pin
                            $("#gutter_pin_" + $pin.data("result").PointNumber).addClass("pointer_hover");

                            // show dialog
                            var showDialog = function () {
                                $map.dialog.bind($pin.data("result"));
                                $map.dialog.show($pin.position().top - 25, $pin.position().left + $pin.width() + 5, $pin.offsetParent());
                            };

                            // delay dialog show
                            // hack: setTimeout firing twice. cancel all timeouts set by hover by setting array of timeout ids
                            dialogTimeoutId.push(window.setTimeout(showDialog, 500));
                        },
                        function (e) {
                            
                            //console.log("get timeoutId = %o", dialogTimeoutId);
                            if (dialogTimeoutId !== undefined && dialogTimeoutId !== null) {
                                $.each(dialogTimeoutId, function (i, val) {
                                    window.clearTimeout(val);
                                });
                                dialogTimeoutId = [];
                            }


                            // de-highlight gutter pin
                            var pinData = $(this).data("result");
                            $("#gutter_pin_" + pinData.PointNumber).removeClass("pointer_hover");
                        }
                    );
        };
        var bind = function (results, callback) {

            // get pin template
            var halfWidth = Math.floor($map.$pinTemplate.width() / 2);
            var height = Math.floor($map.$pinTemplate.height());
            $map.$pinTemplate.hide();

            // pin number sequence
            var num = $("li.gutter_item").length;

            for (var i = 0; i < results.length; i++) {

                // create pin from template and place on map
                var pin = $map.$pinTemplate.clone(true)
                        .attr("id", "pin_" + results[i].PointNumber)
                        .html(num + i + 1)
                        .data("result", results[i])
                        .appendTo("#map_wrapper")
                        .css({ 'top': (parseFloat(results[i].YCoordinate) - height) + "px", 'left': (parseFloat(results[i].XCoordinate) - halfWidth) + "px" })
                        .show();

                // create and append gutter item from result
                var gutterItem = $map.utils.parseTemplate($("#gutter_template").html(), { result: results[i] });
                $("#gutter_items").append(gutterItem);

                // create gutter pin and append to gutter item, don't clone events
                var pin2 = $map.$pinTemplate.clone(false)
                        .attr("id", "gutter_pin_" + results[i].PointNumber)
                        .html(num + i + 1)
                        .data("result", results[i])
                        .css({ 'top': "5px", 'left': null })
                        .prependTo("#gutter_item_" + results[i].PointNumber)
                /*.hover(
                function () {
                // highlight map pin
                var pinData = $(this).data("result");
                $("#pin_" + pinData.PointNumber).addClass("pointer_hover");
                },
                function () {
                // hide dialog if present
                //$map.dialog.hide(500);
                // de-highlight map pin
                var pinData = $(this).data("result");
                $("#pin_" + pinData.PointNumber).removeClass("pointer_hover");
                }
                )*/
                        .click(function (e) {
                            // show dialog
                            e.preventDefault();

                            var pinData = $(this).data("result");
                            var $mapPin = $("#pin_" + pinData.PointNumber);

                            // scroll map to center associated pin
                            $map.map.focusPin($mapPin.offset().top, $mapPin.offset().left, 500, function () {
                                // bind and show dialog with pin data
                                $map.dialog.bind(pinData);
                                $map.dialog.show($mapPin.position().top - 25, $mapPin.position().left + $mapPin.width() + 5, $mapPin.offsetParent());
                                $map.map.lazyload();
                            });
                        })
                        .show();

            } // end for

            // highlight associated pins on gutter item hover
            $("li.gutter_item").hover(
                        function () {
                            var pin = $(this).find(".pointer");
                            var data = pin.data("result");
                            $("#gutter_pin_" + data.PointNumber + ",#pin_" + data.PointNumber).addClass("pointer_hover");
                        },
                        function () {
                            var pinData = $(this).find(".pointer").data("result");
                            $("#gutter_pin_" + pinData.PointNumber + ",#pin_" + pinData.PointNumber).removeClass("pointer_hover");
                        }
                    );

            // fire callback
            callback();
        };
        var load = function (data, callback) {
            // fetch points of interest data
            $.ajax({
                url: $map.settings.serviceUrl,
                data: data,
                error: function (xhr, statusText, error) {
                    if (typeof console !== "undefined") {
                        console.log(xhr);
                    }
                },
                success: function (results, statusText, xhr) {
                    // create and place pins on map
                    bind(results, callback);
                }
            });
        };

        // returns positions array of {id:0, x:0, y:0}
        var reposition = function ($pins, multiplier, positions) {

            // console.log("pins.reposition");

            // use passed-in positions
            if (positions !== undefined && positions !== null && positions.length > 0) {
                $.each(positions, function (i, position) {
                    $("#" + position.id).css({
                        "top": position.y,
                        "left": position.x
                    });
                });
                return positions;
            }

            //console.log("not using stored positions, multiplier = %d", multiplier);

            var top = function ($this) {

                var int = 0;

                if (multiplier < 1) {
                    // zooming out
                    int = ((parseInt($this.css("top")) * multiplier) - ($this.height() * 0.25));
                }
                else if (multiplier > 1) {
                    // zooming in
                    int = ((parseInt($this.css("top")) * multiplier) + ($this.height() * 0.25));
                }
                else {
                    // multiplier === 1, restore position from embedded data
                    int = (parseFloat($this.data("result").YCoordinate) - $this.height());
                }

                //                if ($this.attr("id") === "pin_183") {
                //                    console.log("top int = %d", Math.floor(int));
                //                }

                return Math.floor(int).toString() + "px";
            };
            var left = function ($this) {

                var int = 0;

                if (multiplier < 1) {
                    // zooming out
                    //                    int = ((parseInt($this.css("left")) * multiplier) - ($this.width() * 0.25));
                    int = ((parseInt($this.css("left")) * multiplier) - ($this.width() * 0.125));
                }
                else if (multiplier > 1) {
                    // zooming in
                    //int = ((parseInt($this.css("left")) * multiplier) + ($this.width() * 0.25));
                    int = ((parseInt($this.css("left")) * multiplier) + ($this.width() * 0.125));
                }
                else {
                    // multiplier === 1, restore position from pin data
                    int = (parseFloat($this.data("result").XCoordinate) - ($this.width() * 0.5));
                }

                //                if ($this.attr("id") === "pin_183") {
                //                    console.log("left int = %d", Math.floor(int));
                //                }

                return Math.floor(int).toString() + "px";
            };

            // relocate pins
            // note: can only be called after all pins loaded
            var calculatedPositions = [];
            $pins.each(function (i, el) {

                var $this = $(this);
                if ($this.attr("id") === "pin_template") {
                    return;
                }

                var position = { id: $this.attr("id"), x: left($this), y: top($this) };
                $this.css({
                    "top": position.y,
                    "left": position.x
                });
                calculatedPositions.push(position);
            });
            return calculatedPositions;
        };
        return {
            init: function () { init(); },
            bind: function (results) { bind(results); },
            load: function (data, callback) { load(data, callback); },
            reposition: function ($pins, multiplier, positions) { return reposition($pins, multiplier, positions) }
        };
    } ();
})();


$(document).ready(function () {

    // set ajax defaults
    $.ajaxSetup({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        timeout: 10000,
        data: "{}",
        dataFilter: function (data) {
            var msg;
            if (typeof (JSON) !== 'undefined' && typeof (JSON.parse) === 'function') {
                msg = JSON.parse(data);
            }
            else {
                msg = eval('(' + data + ')');
            }

            if (msg.hasOwnProperty('d')) {
                return msg.d;
            }
            else {
                return msg;
            }
        }
    });

    $map.map.init();
    $map.blurb.init();
    $map.compass.init();
    $map.nav.init();
    $map.pins.init();

    var loadSector = function (sector, callback) {
        var sector_number = parseInt(sector);
        $map.pins.load("{'map':'" + $map.settings.map + "', 'sector':'" + sector_number + "'}", callback);
    };

    //fetch sectors sequentially using callbacks, i.e. only fetch next sector once previous is finished
    loadSector(1, function () {
        loadSector(2, function () {
            loadSector(3, function () {
                loadSector(4, function () {
                    loadSector(5, function () {
                        loadSector(6, function () {
                        });
                    });
                });
            });
        });
    });

    // + delta is up (zoom in)
    // - delta is down (zoom out)
    // higher delta is mouse scroll speed - ignore
    $("#map_viewport").mousewheel(function (e, delta) {
        $map.map.incrementZoom(delta < 0 ? -1 : 1);
        $map.map.revertMargin({ speed: 0 });
    });

    // todo: move to zoom.init
    $("#zoom").slider({
        orientation: "vertical",
        min: $map.settings.zoom.levels[0].increment,
        max: $map.settings.zoom.levels[$map.settings.zoom.levels.length - 1].increment,
        value: 100,
        step: 25,
        change: function (e, ui) {
            // todo: call incrementZoom here, don't do a one-off
            // ui.value is goto zoom

            // prezoom
            $map.$dialog.hide();
            
                $map.settings.zoom.levels[$.inArray(ui.value, $map.settings.zoom.increments)].positions = $map.map.zoom($map.settings.zoom.current, ui.value);
                $map.settings.zoom.current = ui.value;
                $map.map.revertMargin({ speed: 0 });

            
        }
    });
    $("#zoom_plus").click(function () {
        var next = $("#zoom").slider("value") + $("#zoom").slider("option", "step");
        var max = $("#zoom").slider("option", "max");
        if (next > max) { return; }
        $("#zoom").slider("value", next);
    });
    $("#zoom_minus").click(function () {
        var next = $("#zoom").slider("value") - $("#zoom").slider("option", "step");
        var min = $("#zoom").slider("option", "min");
        if (next < min) { return; }
        $("#zoom").slider("value", next);
    });

});



