/*!
 * Copyright (c) 2008, EveryBlock. All rights reserved.
 */

/*global $j, OpenLayers, document, window */

OpenLayers.Map.prototype.getOffset = function() {
    var offset = $j(this.div).offset();
    return new OpenLayers.Pixel(offset.left, offset.top);
};

/*
 * Work-around a placement bug in OpenLayers that shifts the
 * positioning of small marker icons from where they should be
 * placed on the map.
 */
OpenLayers.Icon.prototype._draw = OpenLayers.Icon.prototype.draw;

OpenLayers.Icon.prototype.draw = function(px) {
    var result = OpenLayers.Icon.prototype._draw.apply(this, arguments);
    this.imageDiv.childNodes[0].style.position = "absolute";
    return result;
};

Function.prototype.memoized = function(key) {
    this._values = this._values || {};
    return this._values[key] !== undefined ?
        this._values[key] :
        this._values[key] = this.apply(this, arguments);
};

Function.prototype.memoize = function() {
    var fn = this;
    return function() {
        return fn.memoized.apply(fn, arguments);
    };
};

String.prototype.capitalize = function() {
    return this.replace(/\w+/g, function(a) {
        return a.charAt(0).toUpperCase() + a.substr(1).toLowerCase();
    });
};

OpenLayers.ImgPath = "http://media.everyblock.com/images/openlayers/";

var eb = {
    SCALES: [614400, 307200, 153600, 76800, 38400, 19200, 9600, 4800, 2400, 1200],
    PROJECTION: "EPSG:900913",
    BBOX: new OpenLayers.Bounds(-124.848974, 24.396308, -66.885075, 49.384358), // USA
    IMAGE_ROOT: "http://media.everyblock.com/images",
    TILE_VERSION: "1.1",
    LOCATOR_VERSION: "1.0",
    maps: {},
    Control: {},
    style: {},
    iconSizes: [15, 21, 27, 39, 51],
    DEFAULT_BG_COLOR: "#F4FAF6"
};

// Override the pink used for missing tiles
OpenLayers.Util.onImageLoadErrorColor = eb.DEFAULT_BG_COLOR;

eb.iconList = [];
for (var i = 0, len = eb.iconSizes.length; i < len; i++) {
    var s = eb.iconSizes[i];
    var url = eb.IMAGE_ROOT + "/marker" + s + ".png";
    var size = new OpenLayers.Size(s, s);
    var offset = new OpenLayers.Pixel(-(size.w / 2), -(size.h / 2));
    eb.iconList.push(new OpenLayers.Icon(url, size, offset));
}

eb.style.hilite = {
    fillColor: "#F15A24",
    fillOpacity: 0.75,
    hoverFillColor: "white",
    hoverFillOpacity: 0.8,
    strokeColor: "#B23812",
    strokeOpacity: 0.75,
    strokeWidth: 1,
    strokeLinecap: "round",
    hoverStrokeColor: "red",
    hoverStrokeOpacity: 1,
    hoverStrokeWidth: 0.2,
    pointRadius: 6,
    hoverPointRadius: 1,
    hoverPointUnit: "%",
    pointerEvents: "visiblePainted"
};

eb.style.lite_hilite = function() {
    var style = OpenLayers.Util.extend({}, eb.style.hilite);
    style.fillOpacity = 0.4;
    style.strokeOpacity = 0.4;
    return style;
}();

eb.style.transparent = function() {
    var style = OpenLayers.Util.extend({}, eb.style.hilite);
    style.fillOpacity = 0;
    style.strokeOpacity = 0;
    return style;
}();

eb.style.locator = function() {
    var style = OpenLayers.Util.extend({}, eb.style.hilite);
    style.fillColor = "#009844";
    style.fillOpacity = 1.0;
    style.strokeWidth = 0;
    return style;
}();

eb.cities = {
    chicago: {
        maxExtent: new OpenLayers.Bounds(-88.57973, 41.56685, -86.83981, 42.11301),
        cityView: new OpenLayers.Bounds(-87.940087, 41.644555, -87.524137, 42.023031),
        locatorExtent: new OpenLayers.Bounds(-87.98610, 41.64455, -87.47813, 42.02303),
        locatorScale: 2137198 // in Spherical Mercator
    },
    sf: {
        maxExtent: new OpenLayers.Bounds(-123.425901, 36.940007, -121.369798, 38.174775),
        cityView: new OpenLayers.Bounds(-122.522949, 37.718179, -122.362315, 37.80861),
        locatorExtent: new OpenLayers.Bounds(-122.51521, 37.70777, -122.35644, 37.83327),
        locatorScale: 667048 // in Spherical Mercator
    },
    nyc: {
        maxExtent: new OpenLayers.Bounds(-74.923622, 39.902301, -72.867519, 41.0867951),
        cityView: new OpenLayers.Bounds(-74.266977, 40.488915, -73.713030, 40.920121),
        locatorExtent: new OpenLayers.Bounds(-74.25589, 40.49515, -73.70000, 40.91655),
        locatorScale: 2393203 // in Spherical Mercator
    },
    charlotte: {
        maxExtent: new OpenLayers.Bounds(-81.745, 34.635, -79.927, 35.805),
        cityView: new OpenLayers.Bounds(-81.009628, 35.013208, -80.670058, 35.393150),
        locatorExtent: new OpenLayers.Bounds(-81.07233, 35.01321, -80.60735, 35.39315),
        locatorScale: 1956347 // in Spherical Mercator
    },
    philly: {
        maxExtent: new OpenLayers.Bounds(-75.969, 39.475, -74.275, 40.566),
        cityView: new OpenLayers.Bounds(-75.280305, 39.875063, -74.957522, 40.137927),
        locatorExtent: new OpenLayers.Bounds(-75.29050, 39.87506, -74.94732, 40.13793),
        locatorScale: 1488045 // in Spherical Mercator
    },
    dc: {
        maxExtent: new OpenLayers.Bounds(-77.819759, 38.391645, -76.409393, 39.495548),
        cityView: new OpenLayers.Bounds(-77.119759, 38.791645, -76.909393, 38.995548),
        locatorExtent: new OpenLayers.Bounds(-77.14557, 38.79164, -76.88359, 38.99555),
        locatorScale: 1102246 // in Spherical Mercator
    },
    sanjose: {
        cityView: new OpenLayers.Bounds(-122.056066, 37.127588, -121.539656, 37.464053),
        maxExtent: new OpenLayers.Bounds(-123.556066, 36.627588, -120.039656, 37.964053),
        locatorExtent: new OpenLayers.Bounds(-122.05607, 37.09032, -121.53966, 37.50113),
        locatorScale: 2172719 // in Spherical Mercator
    },
    boston: {
        cityView: new OpenLayers.Bounds(-71.191153, 42.227865, -70.986487, 42.396978),
        maxExtent: new OpenLayers.Bounds(-72.312, 41.503, -69.849, 43.203),
        locatorExtent: new OpenLayers.Bounds(-71.20317, 42.22786, -70.97447, 42.39698),
        locatorScale: 962181 // in Spherical Mercator
    },
    seattle: {
        cityView: new OpenLayers.Bounds(-122.459696, 47.491912, -122.224433, 47.734145),
        maxExtent: new OpenLayers.Bounds(-124.478713, 46.427592, -120.637643, 48.319393),
        locatorExtent: new OpenLayers.Bounds(-122.51294, 47.49551, -122.15895, 47.73414),
        locatorScale: 1511808 // in Spherical Mercator
    },
    la: {
        cityView: new OpenLayers.Bounds(-118.668171, 33.704902, -118.155368, 34.337307),
        maxExtent: new OpenLayers.Bounds(-119.668171, 33.204902, -117.155368, 34.837307),
        locatorExtent: new OpenLayers.Bounds(-118.79328, 33.70490, -118.03026, 34.33731),
        locatorScale: 3210274 // in Spherical Mercator
    },
    miami: {
        cityView: new OpenLayers.Bounds(-80.87360, 25.13742, -80.042754, 25.979434),
        maxExtent: new OpenLayers.Bounds(-82.012, 24.448, -79.236, 26.497),
        locatorExtent: new OpenLayers.Bounds(-80.92486, 25.13742, -79.99150, 25.97943),
        locatorScale: 3926966 // in Spherical Mercator
    },
    atlanta: {
	cityView: new OpenLayers.Bounds(-84.546992, 33.647838, -84.289388, 33.887585),
	maxExtent: new OpenLayers.Bounds(-84.546992, 33.647838, -84.289388, 33.887585),
	locatorExtent: new OpenLayers.Bounds(-84.56239, 33.64784, -84.27399, 33.88758),
	locatorScale: 1213405
    },
    dallas: {
	cityView: new OpenLayers.Bounds(-97.000412, 32.61828, -96.46374, 33.02170),
	maxExtent: new OpenLayers.Bounds(-97.000412, 32.61828, -96.46374, 33.02170),
	locatorExtent: new OpenLayers.Bounds(-97.00041, 32.59443, -96.46374, 33.04544),
	locatorScale: 2257977
    },
    detroit: {
	cityView: new OpenLayers.Bounds(-83.28782, 42.25552, -82.91026, 42.45038),
	maxExtent: new OpenLayers.Bounds(-83.28782, 42.25552, -82.91026, 42.45038),
	locatorExtent: new OpenLayers.Bounds(-83.28782, 42.21336, -82.91026, 42.49238),
	locatorScale: 1588528
    },
    houston: {
	cityView: new OpenLayers.Bounds(-95.910106, 29.537381, -95.014574, 30.110706),
	maxExtent: new OpenLayers.Bounds(-95.910106, 29.537381, -95.014574, 30.110706),
	locatorExtent: new OpenLayers.Bounds(-95.91011, 29.43524, -95.01457, 30.21216),
	locatorScale: 3767819
    }
};

eb.PanZoom = OpenLayers.Class(OpenLayers.Control.PanZoom, {
    includeButtons: {
	"zoomin": {
	    outImageSrc: "zoom-plus-mini.png",
	    overImageSrc: "zoom-plus-mini-over.png"
	},
	"zoomout": {
	    outImageSrc: "zoom-minus-mini.png",
	    overImageSrc: "zoom-minus-mini-over.png"
	}
    },

    makeMouseCallback: function(id, state) {
	var selector = state + "ImageSrc";
	var src = OpenLayers.Util.getImagesLocation() + this.includeButtons[id][selector];
	// 'this' here is bound to the div containing the button img
	return function(evt) {
	    var img = this.firstChild;
	    if (img.src !== src) {
		img.src = src;
	    }
	};
    },

    _addButton: function(id, img, xy, sz) {
        if (id in this.includeButtons) {
	    var src = this.includeButtons[id].outImageSrc;
	    var size = new OpenLayers.Size(20, 20);
            var btn = OpenLayers.Control.PanZoom.prototype._addButton.call(this, id, src, xy, size);
	    btn.className = this.displayClass + id.capitalize();
	    btn._btnId = id;
	    OpenLayers.Event.observe(btn, "mouseover", OpenLayers.Function.bindAsEventListener(this.makeMouseCallback(id, "over"), btn));
	    OpenLayers.Event.observe(btn, "mouseout", OpenLayers.Function.bindAsEventListener(this.makeMouseCallback(id, "out"), btn));
	    return btn;
        }
    },

    CLASS_NAME: "eb.PanZoom"
});

eb.Control.Navigation = OpenLayers.Class(OpenLayers.Control.Navigation, {
    activate: function() {
        this.dragPan.activate();
        this.zoomBox.activate();
        return OpenLayers.Control.prototype.activate.apply(this, arguments);
    }
});

eb.maps.baseMap = OpenLayers.Class(OpenLayers.Map, {
    // flag for use by other functions when debugging, sort of a global state
    debug: false,

    initialize: function(div, city_slug, options) {
        this.city = eb.cities[city_slug];
        var maxExtent = eb.BBOX.clone().transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection(eb.PROJECTION));
        var baseOptions = {
            "projection": new OpenLayers.Projection(eb.PROJECTION),
            "displayProjection": new OpenLayers.Projection("EPSG:4326"),
            "maxExtent": maxExtent,
            "scales": eb.SCALES,
            "controls": [],
            "units": "m",
	    "theme": null
        };
	OpenLayers.Util.extend(baseOptions, options);
	OpenLayers.Map.prototype.initialize.call(this, div, baseOptions);
    },

    addTileLayer: function() {
        var layer = new eb.TileLayer("main", eb.TILE_URL, {
            "units": "m",
            "version": eb.TILE_VERSION,
            "layername": "main",
            "type": "png",
            "buffer": 1
        });
        this.tiles = layer;
        this.addLayer(this.tiles);
    },

    recenter: function(lat, lng, zoom) {
        var latlng = new OpenLayers.LonLat(lng, lat).transform(this.displayProjection,
                                                               this.getProjectionObject());
        if (zoom === undefined) {
            zoom = this.DEFAULT_ZOOM;
        }
        var dragging = false;
        this.setCenter(latlng, zoom, dragging);
    },

    setCenter: function(lonlat, zoom) {
        if (zoom === undefined) {
            zoom = this.DEFAULT_ZOOM;
        }
        OpenLayers.Map.prototype.setCenter.call(this, lonlat, zoom);
    },

    zoomToBbox: function(minx, miny, maxx, maxy) {
        var extent = new OpenLayers.Bounds(minx, miny, maxx, maxy).
	    transform(this.displayProjection, this.getProjectionObject());
        this.zoomToExtent(extent);
    },

    addMarkerLayer: function(name) {
        var layerName = (name) ? name : "Markers";
        this.markers = new OpenLayers.Layer.Markers(layerName);
        this.addLayer(this.markers);
    },

    addMarker: function(lnglat, icon) {
        // TODO: this method shouldn't know about marker layer
        if (this.markers === undefined) {
            this.addMarkerLayer();
        }
        var marker = new eb.Marker(lnglat.transform(this.displayProjection, this.getProjectionObject()), icon);
        this.markers.addMarker(marker);
    },

    addFeatures: function(layer, serialized, format, style) {
        var features = this.createFeatures(serialized, format, style);
        layer.addFeatures(features);
    },

    zoomToCityView: function() {
        var extent = this.city.cityView.clone();
        extent.transform(this.displayProjection, this.getProjectionObject());
        this.zoomToExtent(extent);
    },

    zoomToCityMaxExtent: function() {
        var extent = this.city.maxExtent.clone();
        extent.transform(this.displayProjection, this.getProjectionObject());
        this.zoomToExtent(extent);
    },

    closePopups: function() {
        for (var i = 0, len = this.popups.length; i < len; i++) {
            var popup = this.popups[i];
            popup.removeFromMap();
        }
    },

    addPopup: function(popup, exclusive) {
        OpenLayers.Map.prototype.addPopup.apply(this, arguments);
        this.events.triggerEvent("popupopen");
    },

    removePopup: function(popup) {
        OpenLayers.Map.prototype.removePopup.apply(this, arguments);
        this.events.triggerEvent("popupclose");
    },

    currentPopup: function() {
        if (this.popups.length) {
            return this.popups[0];
        }
        return null;
    }
});

eb.ImageTile = OpenLayers.Class(OpenLayers.Tile.Image, {
    coord: null,

    initialize: function() {
	OpenLayers.Tile.Image.prototype.initialize.apply(this, arguments);
	this.coord = this.getCoordinate();
    },

    draw: function() {
	var result = OpenLayers.Tile.Image.prototype.draw.apply(this, arguments);
	this.coord = this.getCoordinate();
	return result;
    },

    getCoordinate: function() {
	return this.layer.getCoordinate(this.bounds);
    }
});

eb.TileLayer = OpenLayers.Class(OpenLayers.Layer.TMS, {
    version: null, // see eb.TILE_VERSION
    layername: null, // lower-cased: "main", "locator"
    type: null, // i.e., mime-type extension: "png", "jpg", "gif"

    initialize: function(name, url, options) {
        var args = [];
        args.push(name, url, {}, options);
        OpenLayers.Layer.Grid.prototype.initialize.apply(this, args);
    },

    addTile: function(bounds, position) {
        return new eb.ImageTile(this, position, bounds, null, this.tileSize);
    },

    // Returns an object with the x, y, and z of a tile for a given bounds
    getCoordinate: function(bounds) {
	bounds = this.adjustBounds(bounds);
	var res = this.map.getResolution();
	var x = Math.round((bounds.left - this.tileOrigin.lon) / (res * this.tileSize.w));
	var y = Math.round((bounds.bottom - this.tileOrigin.lat) / (res * this.tileSize.h));
	var z = this.map.getZoom();
	return {x: x, y: y, z: z};
    },

    getPath: function(x, y, z) {
        return this.version + "/" + this.layername + "/" + z + "/" + x + "," + y + "." + this.type;
    },

    getURL: function(bounds) {
	var coord = this.getCoordinate(bounds);
        var path = this.getPath(coord.x, coord.y, coord.z);
        var url = this.url;
        if (url instanceof Array) {
            url = this.selectUrl(path, url);
        }
        return url + path;
    },

    CLASS_NAME: "eb.TileLayer"
});

eb.maps.detailMap = OpenLayers.Class(eb.maps.baseMap, {
    DEFAULT_ZOOM: 6,
    hasLocationClickHandling: false,
    useOverlayPopups: false,

    initialize: function(div, city_slug, options) {
        eb.maps.baseMap.prototype.initialize.apply(this, arguments);

	this.addTileLayer();

        var controls = [new OpenLayers.Control.DragPan(),
                        new eb.PanZoom(),
                        new OpenLayers.Control.ArgParser(),
                        new OpenLayers.Control.Navigation({zoomWheelEnabled: false})];
        for (var i = 0, len = controls.length; i < len; i++) {
            this.addControl(controls[i]);
            controls[i].activate();
        }

        // Assign a reference to this map object to "self" for use in
        // the callbacks following
        var self = this;
        this.eventCallbacks = {
            "normal": {
                "zoomend": function(event) {
                    self.closePopups();
                    if (self.scaleBunches) {
                        self.draw();
                    }
                },
                // If the map is moving (dragging, zooming, etc.), we
                // don't want any open popups to close.
                "move": function(event) {
                    if (self.popups.length) {
                        self.dontClosePopups = true;
                    }
                }
            },
            "priority": {
                // Clicking on the map will clear any open popups
                "click": function(event) {
                    if (!self.dontClosePopups) {
                        self.closePopups();
                    }
                    self.dontClosePopups = false;
                }
	    }
	};

	this.events.on(this.eventCallbacks.normal);
	for (var key in this.eventCallbacks.priority) {
	    if (true) { // bypass jslint warning
		this.events.registerPriority(key, this, this.eventCallbacks.priority[key]);
	    }
	}

        if (typeof $j !== "undefined") {
            this.addLocationClickHandling();
        }
    },

    getMarkerById: function(id) {
        for (var i = 0, len = this.markers.markers.length; i < len; i++) {
            var marker = this.markers.markers[i];
            if (marker.id === id) {
                return marker;
            }
        }
        return null;
    },

    addNewsItem: function(id, lat, lng, popupHtml) {
        if (popupHtml === undefined) {
            popupHtml = "";
        }
        var latlng = new OpenLayers.LonLat(lng, lat);
        var marker = new eb.Marker(latlng);
        marker.id = id;
        marker.popupHtml = popupHtml;
        var markerLayer = this.getMarkerLayer();
        if (markerLayer === null) {
            markerLayer = new OpenLayers.Layer.Markers("markers");
            this.addLayer(markerLayer);
        }
        markerLayer.addMarker(marker);
    },

    /*
     * Returns the first layer found of Markers type
     */
    getMarkerLayer: function() {
        var markerLayer = null;
        for (var i = 0, len = this.layers.length; i < len; i++) {
            var layer = this.layers[i];
            if (layer.CLASS_NAME === "OpenLayers.Layer.Markers") {
                markerLayer = layer;
            }
        }
        return markerLayer;
    },

    /*
     * Zooms map to an extent containing all the visible markers
     */
    zoomToMarkers: function() {
        var layer = this.getMarkerLayer();
        if (layer) {
            var bounds = new OpenLayers.Bounds();
            for (var i = 0, len = layer.markers.length; i < len; i++) {
                bounds.extend(layer.markers[i].lonlat);
            }
            this.zoomToExtent(bounds);
        }
    },

    getClusterLayer: function() {
        var clusterLayer = null;
        for (var i = 0, len = this.layers.length; i < len; i++) {
            var layer = this.layers[i];
            if (layer.CLASS_NAME === "eb.ClusterLayer") {
                clusterLayer = layer;
            }
        }
        return clusterLayer;
    },

    /*
     * Zooms map to an extent containing all the visible clusters.
     */
    zoomToClusters: function() {
        var layer = this.getClusterLayer();
        var bounds = new OpenLayers.Bounds();
        if (layer) {
            // We want to create an extent based on the lng/lats of
            // the smallest scale.
            var scale = eb.SCALES[eb.SCALES.length-1];
            var bunches = layer.bunches[scale];
            for (var i = 0, len = bunches.length; i < len; i++) {
                var bunch = bunches[i];
                // Element 0 of the bunch is a list of IDs,
                // element 1 is the lng/lat pair.
                bounds.extend(new OpenLayers.LonLat(bunch[1][0], bunch[1][1]));
            }
        }
        if (bounds.toBBOX() === '0,0,0,0') {
            bounds = this.city.cityView.clone();
        }
        bounds.transform(this.displayProjection, this.getProjectionObject());
        this.zoomToExtent(bounds);
	// Don't zoom in past the default zoom level -- it can be too
	// far in to resolve where you are
	if (this.getZoom() > this.DEFAULT_ZOOM) {
	    console.log("rezooming");
	    this.setCenter(this.getCenter(), this.DEFAULT_ZOOM);
	}
    },


    /*
     * Allow the user to click on the location string
     * in the newsitem list and have the correspoonding
     * newsitem's map marker popup pop on the map.
     */
    addLocationClickHandling: function() {
        var map = this;
        var locations = $j(".newsitemlist:not(.noclick) span.location");
        if (locations.length) {
            map.hasLocationClickHandling = true;
        }
        locations.click(function() {
            var attr_id = $j(this).parent("li").attr("id");
            var id = attr_id.substring(attr_id.lastIndexOf("-") + 1) * 1;
            if (!$j(this).hasClass("selected")) {
                var layer = map.getClusterLayer();
                var marker_tuple = layer.findMarkerByObj(id);
                if (marker_tuple) {
                    $j("span.location").removeClass("selected");
                    $j(this).addClass("selected");
                    var marker = marker_tuple[0];
                    var obj_idx = marker_tuple[1];
                    marker.events.triggerEvent("click");
                    // If obj_idx is 0, then it's the first (or only) object
                    // in the marker cluster, and so we don't need to advance
                    // the paginator
                    if (obj_idx !== 0) {
                        if (map.popups.length !== 0) {
                            map.popups[0].setPage(obj_idx);
                        }
                    }
                    if (!marker.onScreen()) {
                        map.setCenter(marker.lonlat, map.getZoom(), true, false);
                    }
                    window.location.hash = "popup-" + id;
                }
            } else {
                $j("#popup-" + id).find("a.closebutton").click();
            }
        });
    }
});

eb.ClusterLayer = OpenLayers.Class(OpenLayers.Layer.Markers, {
    bunches: null,
    popupContentFetcher: null,

    /*
     * Parameters:
     * name - layer name
     */
    initialize: function(name, baseOptions, popupContentFetcher) {
        OpenLayers.Layer.Markers.prototype.initialize.call(this, name, baseOptions);
        this.popupContentFetcher = popupContentFetcher;
        this.bunches = {};
    },

    addBunches: function(bunches) {
        this.bunches = bunches;
    },

    addFilter: function(fn) {
	this.filter = fn;
    },

    clearFilter: function() {
	delete this.filter;
    },

    /*
     * Argument is a bunch object represented by an array, not a
     * eb.Bunch object (confusing, yes) -- this is the data structure
     * created by JSON encoding coming from the application, and we filter
     * the objects in the payload of the array before it is passed to the
     * eb.Bunch initialization
     */
    filterObjects: function(bunch) {
	if (!this.filter) {
	    return bunch;
	}
	var passed = [];
	for (var i = 0; i < bunch[0].length; i++) {
	    var obj = bunch[0][i];
	    if (this.filter(obj)) {
		passed.push(obj);
	    }
	}
	if (passed.length) {
	    return [passed, bunch[1]];
	} else {
	    return null;
	}
    },

    moveTo: function(bounds, zoomChanged, dragging) {
        OpenLayers.Layer.prototype.moveTo.apply(this, arguments);
        if (zoomChanged || !this.drawn) {
            this.clearMarkers();
            var scale = this.map.getScale();
            var PopupClass = this.map.useOverlayPopups ? eb.PaginatedOverlayPopup : eb.PaginatedPopup;
            for (var i = 0, len = this.bunches[scale].length; i < len; i++) {
		var bunchJson = this.filterObjects(this.bunches[scale][i]);
		if (bunchJson) {
		    var bunch = eb.bunchFromJSON(this.filterObjects(this.bunches[scale][i]));

                    // Transform projection
                    var markerPosition = OpenLayers.Projection.transform(
			{ x: bunch.lonlat.lon, y: bunch.lonlat.lat },
			this.map.displayProjection,
			this.map.getProjectionObject()
                    );
                    bunch.lonlat = new OpenLayers.LonLat(markerPosition.x, markerPosition.y);

                    this.markers.push(bunch);
                    bunch.map = this.map;
                    this.drawMarker(bunch);
                    bunch.drawNumber();

                    // Set up popup
                    bunch.events.register("click", this, function(map, layer, bunch, PopupClass) {
			return function(evt) {
			    if (!map.dontClosePopups) {
				map.closePopups();
				var popup = new PopupClass(null, bunch.lonlat, bunch.icon,
							   layer.popupContentFetcher(bunch.objs));
				popup.addToMap(map);
				popup.updatePosition();
				popup.addPagination();
				if (map.hasLocationClickHandling) {
				    var ni_id = bunch.objs[0];
				    $j("#newsitem-" + ni_id + " span.location").addClass("selected");
				}
				OpenLayers.Event.stop(evt);
                            }
                            map.dontClosePopups = false;
			};
		    }(this.map, this, bunch, PopupClass));
		}
            }
            this.drawn = true;
        }
    },

    /*
     * Returns an array, the first item is the marker representing
     * the bunch in which the object being sought is contained,
     * and the second item is the index of the position of the
     * object in the array of the marker's objects.
     */
    findMarkerByObj: function(obj) {
        for (var i = 0, ilen = this.markers.length; i < ilen; i++) {
            for (var j = 0, jlen = this.markers[i].objs.length; j < jlen; j++) {
                if (obj === this.markers[i].objs[j]) {
                    return [this.markers[i], j];
                }
            }
        }
        return null;
    },

    CLASS_NAME: "eb.ClusterLayer"
});

eb.setIcon = function(marker, icon) {
    marker.icon.imageDiv.firstChild.src = marker.icon.url = icon.url;
};

eb.cloneMarker = function(marker, icon) {
    icon = (icon) ? icon : marker.icon.clone();
    var newMarker = new OpenLayers.Marker(marker.lonlat, icon);
    var attrs = ["id", "events"];
    for (var i = 0, len = i < attrs.length; i < len; i++) {
        var attr = attrs[i];
        newMarker[attr] = marker[attr];
    }
    return newMarker;
};

eb.maps.contextMap = OpenLayers.Class.create();
eb.maps.contextMap.prototype = OpenLayers.Class.inherit(eb.maps.baseMap, {
    initialize: function(div, city_slug, options) {
        this.city = eb.cities[city_slug];
        var extent = this.city.locatorExtent;
        extent.transform(new OpenLayers.Projection("EPSG:4326"),
                 new OpenLayers.Projection(eb.PROJECTION));
        var baseMapOptions = {
            "maxExtent": extent,
            "scales": [this.city.locatorScale]
        };
        eb.maps.baseMap.prototype.initialize.call(this, div, city_slug, baseMapOptions);
        this.boxes = new OpenLayers.Layer.Boxes("boxes");
        var layer = new OpenLayers.Layer.Image(
            "locator",
            eb.IMAGE_ROOT + "/locator/" + city_slug + ".png",
            extent,
            new OpenLayers.Size(75, 75),
            {buffer: 0}
        );
        this.addLayer(layer);
    }
});

eb.Marker = OpenLayers.Class.create();
eb.Marker.prototype = OpenLayers.Class.inherit(OpenLayers.Marker, {
    enabled: true,

    initialize: function(latlng, icon) {
        if (icon === undefined) {
            icon = eb.Icons.defaultIcon.clone();
        }
        OpenLayers.Marker.prototype.initialize.call(this, latlng, icon);
    }
});

/*
 * Zoom-based marker sizing
 *
 * Markers have a "base" size tied to a certain zoom level, and then
 * they are resized by a certain amount when the map zooms in or out.
 */
eb.ZoomBasedMarker = OpenLayers.Class(eb.Marker, {
    /*
     * Parameters:
     * zoomFn - closure that when called (w no args) returns the current map zoom
     */
    initialize: function(lonlat, icon, zoomFn) {
	eb.Marker.prototype.initialize.call(this, lonlat, icon);
	this.baseSize = icon.size.clone();
	this.zoomFn = zoomFn;
    },

    draw: function(px) {
	this.resize(this.zoomFn());
	return eb.Marker.prototype.draw.apply(this, arguments);
    },

    resize: function(zoom) {
	var newSize = this.baseSize.clone();
	newSize.w += 2 * zoom;
	newSize.h += 2 * zoom;
	this.icon.size = newSize;
    }
});

eb.Icons = {
    defaultIcon: function() {
        var size = new OpenLayers.Size(15, 15);
        var offset = new OpenLayers.Pixel(-7.5, -7.5);
        var icon = new OpenLayers.Icon(eb.IMAGE_ROOT + "/marker15.png", size, offset);
        return icon;
    }(),
    offIcon: function() {
        var size = new OpenLayers.Size(15, 15);
        var offset = new OpenLayers.Pixel(-7.5, -7.5);
        var icon = new OpenLayers.Icon(eb.IMAGE_ROOT + "/marker15_off.png", size, offset);
        return icon;
    }(),
    locatorPin: function() {
        var size = new OpenLayers.Size(5, 5);
        var offset = new OpenLayers.Pixel(-2.5, -2.5);
        var icon = new OpenLayers.Icon(eb.IMAGE_ROOT + "/locator_pin.png", size, offset);
        return icon;
    }()
};

eb.createFeatures = function(serialized, format, style) {
    var features = format.read(serialized);
    if (features) {
        if (features.constructor !== Array) {
            features = [features];
        }
        if (style !== undefined) {
            for (var i = 0, len = features.length; i < len; i++) {
                features[i].style = style;
            }
        }
        return features;
    }
};

eb.Bunch = OpenLayers.Class(OpenLayers.Marker, {
    objs: null,

    iconIdx: 0,

    initialize: function(objs, lonlat) {
        this.objs = objs;
        var numIcons = this.objs.length;
        var iconIdx;
        if (numIcons === 1) {
            iconIdx = 0;
        } else if (numIcons >= 2 && numIcons <= 5) {
            iconIdx = 1;
        } else if (numIcons >= 6 && numIcons <= 25) {
            iconIdx = 2;
        } else if (numIcons >= 26 && numIcons <= 50) {
            iconIdx = 3;
        } else if (numIcons > 50) {
            iconIdx = 4;
        }
        this.iconIdx = iconIdx;
        var icon = eb.iconList[this.iconIdx].clone();
        OpenLayers.Marker.prototype.initialize.call(this, lonlat, icon);
    },

    drawNumber: function() {
        var n = this.objs.length;
        var i = this.iconIdx;
        if (n > 1) {
            var markerDiv = this.icon.imageDiv;
            var numDiv = document.createElement("div");
            if (i > 0) {
                numDiv.className = "bunchNum markerSize" + (i + 1);
            } else {
                numDiv.className = "bunchNum";
            }
            numDiv.innerHTML = n;
            numDiv.style.height = markerDiv.style.height;
            numDiv.style.width = markerDiv.style.width;
            numDiv.style.lineHeight = markerDiv.style.height;
            numDiv.style.textAlign = "center";
            numDiv.style.position = "absolute";
            numDiv.style.top = 0;
            numDiv.style.left = 0;
            markerDiv.appendChild(numDiv);
        }
    }
});

/*
 * A helper function that creates a new Bunch object from a JSON
 * representation, which is a list where the first element is a
 * list of objects, and the second elements is a list of [lng, lat].
 */
eb.bunchFromJSON = function(json) {
    var lonlat = new OpenLayers.LonLat(json[1][0], json[1][1]);
    return new eb.Bunch(json[0], lonlat);
};

// Monkey-patch some of OpenLayers.Popup methods so we can have more
// flexibility with positioning, especially with respect to overlays
// and anchored popups.
eb.PopupMonkeypatch = {
    getPixel: function() {
        return this.map.getLayerPxFromLonLat(this.lonlat);
    },

    draw: function(px) {
        if (px === null) {
            if ((this.lonlat !== null) && (this.map !== null)) {
                px = this.getPixel();
            }
        }

        //listen to movestart, moveend to disable overflow (FF bug)
        if (OpenLayers.Util.getBrowserName() == 'firefox') {
            this.map.events.register("movestart", this, function() {
                var style = document.defaultView.getComputedStyle(
                    this.contentDiv, null
                );
                var currentOverflow = style.getPropertyValue("overflow");
                if (currentOverflow != "hidden") {
                    this.contentDiv._oldOverflow = currentOverflow;
                    this.contentDiv.style.overflow = "hidden";
                }
            });
            this.map.events.register("moveend", this, function() {
                var oldOverflow = this.contentDiv._oldOverflow;
                if (oldOverflow) {
                    this.contentDiv.style.overflow = oldOverflow;
                    this.contentDiv._oldOverflow = null;
                }
            });
        }

        this.moveTo(px);
        if (!this.autoSize && !this.size) {
            this.setSize(this.contentSize);
        }
        this.setBackgroundColor();
        this.setOpacity();
        this.setBorder();
        this.setContentHTML();

        if (this.panMapIfOutOfView) {
            this.panIntoView();
        }

        return this.div;
    },

    updatePosition: function() {
        if (this.lonlat && this.map) {
            var px = this.getPixel();
            if (px) {
                this.moveTo(px);
            }
        }
    },

    getOffsetPixel: function(px) {
        return px;
    },

    moveTo: function(px) {
        if ((px !== undefined) && (this.div !== undefined)) {
            var offsetPx = this.getOffsetPixel(px);
            this.div.style.left = offsetPx.x + "px";
            this.div.style.top = offsetPx.y + "px";
        }
    }
};

eb.Popup = OpenLayers.Class(OpenLayers.Popup, eb.PopupMonkeypatch, {
    addToMap: function(map) {
        this.map = map;
        map.addPopup(this);
    },

    removeFromMap: function() {
        this.map.removePopup(this);
    },

    CLASS_NAME: "eb.Popup"
});

eb.AnchoredPopup = OpenLayers.Class(eb.Popup, {
    initialize: function(id, lonlat, anchor, html) {
	this.lonlat = lonlat;
	this.anchor = anchor;
	eb.Popup.prototype.initialize.call(this, id, lonlat, new OpenLayers.Size(0, 0), html, false);
	this.onclick = function() {};
    },

    getAnchorOffset: function() {
        // see /styles/base.css:256
        var popupPadding = 12;
        var arrowLeftOffset = 60;
        var pointerImgWidth = 22;
        var anchorSize = this.anchor.size;
        var w = -(arrowLeftOffset + (pointerImgWidth / 2));
        var h = -this.size.h - popupPadding - (anchorSize.h / 2);
        return new OpenLayers.Size(w, h);
    },

    getOffsetPixel: function(px) {
        var anchorOffset = this.getAnchorOffset();
        return new OpenLayers.Pixel(anchorOffset.w + px.x, anchorOffset.h + px.y);
    }
});

eb.PaginatedPopup = OpenLayers.Class(eb.AnchoredPopup, {
    initialize: function(id, lonlat, anchor, htmlList) {
        if (htmlList.constructor !== Array) {
            htmlList = [htmlList];
        }
        eb.AnchoredPopup.prototype.initialize.call(this, id, lonlat, anchor, "");
        this.updateHtml(htmlList);
    },

    updateHtml: function(htmlList) {
        this.htmlList = htmlList;
        this.current = 0;
        this.setPopup(this.htmlList[0]);
    },

    draw: function() {
        var div = eb.Popup.prototype.draw.apply(this, arguments);
        this.addCloseBox();
        return div;
    },

    setPage: function(page_num) {
        if (page_num === undefined) {
            return null;
        }
        if (page_num < 0) {
            page_num = this.htmlList.length - 1;
        } else if (page_num >= this.htmlList.length) {
            page_num = 0;
        }
        this.current = page_num;
        this.setPopup(this.htmlList[page_num]);
        this.addPagination();
    },

    createIncDecLink: function(className, text, direction) {
        var link = document.createElement("a");
        link.className = className;
        link.innerHTML = text;
        link.events = new OpenLayers.Events(link, link, null);
        link.events.register("click", this, function(page_num) {
            var callback = function(evt) {
                this.setPage(page_num);
                return false;
            };
            return callback;
        }(this.current + direction));
        return link;
    },

    createPagination: function(i, n, className) {
        if (className === undefined) {
            className = "labelpaginator";
        }
        var p = document.createElement("p");
        p.className = className;
        p.appendChild(this.createIncDecLink("prev", "Previous", -1));
        p.appendChild(this.createIncDecLink("next", "Next", 1));
        p.appendChild(document.createTextNode((i + 1) + " of " + n));
        return p;
    },

    addPagination: function() {
        if (this.htmlList.length > 1) {
            var pagination = this.createPagination(this.current, this.htmlList.length);
            var divs = this.contentDiv.getElementsByTagName("div");
            var div;
            for (var i = 0, len = divs.length; i < len; i++) {
                if (/paginated/.test(divs[i].className)) {
                    div = divs[i];
                    break;
                }
            }
            if (div) {
                div.appendChild(pagination);
            }
            if (this.map.hasLocationClickHandling) {
                var attr_id = $j(pagination).parents(".popup").attr("id");
                var id = attr_id.substring(attr_id.lastIndexOf("-") + 1) * 1;
                $j("span.location").removeClass("selected");
                $j("#newsitem-" + id + " span.location").addClass("selected");
            }
        }
    },

    setPopup: function(html) {
        var size;
        try {
            var div = $j(html).appendTo("body");
            var w = div.width();
            var h = div.height();
            div.remove();
            size = new OpenLayers.Size(w, h);
        } catch (e) {
            size = new OpenLayers.Size(300, 200);
        }
        this.setSize(size);
        OpenLayers.Popup.prototype.setContentHTML.call(this, html);
        this.addCloseBox();
        this.updatePosition();
    },

    addCloseBox: function() {
        var closeBox = $j('<a class="closebutton" href="#">Close</a>');
        var map = this.map;
        closeBox.click(function() {
            map.closePopups();
            if (map.hasLocationClickHandling) {
                $j("span.location").removeClass("selected");
            }
            return false;
        });
        closeBox.insertBefore($j(this.div).find("h2.labelheader"));
    },

    CLASS_NAME: "eb.PaginatedPopup"
});

eb.OverlayPopupMixin = {
    createMoveCallback: function(popup) {
        var callback = function(evt) {
            var px = popup.map.getPixelFromLonLat(popup.lonlat);
            popup.moveTo(px);
        };
        return callback;
    },

    addToMap: function(map) {
        this.map = map;
        map.popups.push(this);
        this.div = this.draw();
        document.body.appendChild(this.div);
        this.moveFunc = this.createMoveCallback(this);
        this.map.events.register("move", this, this.moveFunc);
        this.map.events.triggerEvent("popupopen");
    },

    arrowHeight: 12,
    arrowWidth: 22,
    arrowOffset: 30,

    /*
     * Returns an offset that positions the popup above and slightly
     * to the left of the anchor point
     */
    getAnchorOffsetAbove: function() {
        var tmpDiv = $j(this.div).clone().appendTo("body");
        var popupHeight = tmpDiv.height();
        var popupWidth = tmpDiv.width();
        tmpDiv.remove();
        var h = popupHeight + (this.anchor.size.h / 2) + this.arrowHeight;
        var w = popupWidth - ((this.arrowWidth / 2) + this.arrowOffset);
        return new OpenLayers.Size(-w, -h);
    },

    getAnchorOffsetLeft: function() {
        // see .olPopup.rightpointer in /styles/base.css
        var tmpDiv = $j(this.div).clone().appendTo("body");
        var popupWidth = tmpDiv.width();
        tmpDiv.remove();
        var w = popupWidth + (this.anchor.size.w / 2) + 12;
        var h = (this.arrowWidth / 2) + this.arrowOffset;
        return new OpenLayers.Size(-w, -h);
    },

    getAnchorOffset: function() {
        return this.getAnchorOffsetAbove();
    },

    getPixel: function() {
        return this.map.getPixelFromLonLat(this.lonlat);
    },

    setArrowPosition: function(offset) {
        var x = -offset.w - (this.arrowWidth / 2);
        $j(this.div).css('backgroundPosition', x + 'px 100%');
    },

    getOffsetPixel: function(px) {
        var anchorOffset = this.getAnchorOffset();
        this.setArrowPosition(anchorOffset);
        var mapOffset = this.map.getOffset();
        return new OpenLayers.Pixel(anchorOffset.w + mapOffset.x + px.x, anchorOffset.h + mapOffset.y + px.y);
    },

    removeFromMap: function() {
        this.map.events.unregister("move", this, this.moveFunc);
        OpenLayers.Util.removeItem(this.map.popups, this);
        try {
            document.body.removeChild(this.div);
        } catch (e) {
            // no-op
        }
        this.map.events.triggerEvent("popupclose");
        this.map = null;
    }
};

eb.PaginatedOverlayPopup = OpenLayers.Class(eb.PaginatedPopup, eb.OverlayPopupMixin);

/*
 * Helper function that returns a lazily-defined function, one that
 * may have its definition changed when called the first time
 * (perhaps because it is setting up a closure to take advantage
 * of an expensive operation)
 *
 * Parameters:
 * fn - a function that returns the finally defined closure
 *
 */
eb.lazyFn = function(fn) {
    var f;
    return function() {
        if (f) {
            return f.apply(this, arguments);
        }
        f = fn();
        return f.apply(this, arguments);
    };
};

/*
 * Creates a closure that has fetched remote HTML of the popups from
 * a list of newsitem ids, and returns HTML for a subset of ids when
 * called subsequently.
 *
 * The fetching of the remote HTML is delayed until the initial call
 * of the closure.
 */
eb.makeAjaxContentFetcher = function(idsToFetch, map) {
    var url = "/api/map-popups/";

    // TODO: these should ideally live in the HTML and not as
    // string literals here
    var popupHtmlTpl = '' +
        '<div class="popup" id="popup-ni-${newsitem_id}">' +
                '<div class="poplabel maplabel bottomheader paginated">' +
                        '<h2 class="labelheader">${schema}</h2>' +
                        '<div class="labelwrap">' +
                                '<ul class="newsitemlist">' +
                                    '${html}' +
                                '</ul>' +
                        '</div>' +
                '</div>' +
        '</div>';

    var loadingHtml = '' +
        '<div class="popup" id="loading-popup">' +
                '<div class="poplabel maplabel bottomheader">' +
                    '<h2 class="labelheader">Loading &hellip;</h2>' +
                '</div>' +
        '</div>';

    // This `cache' object will hold the HTML strings and schema name
    // from the server response, keyed by newsitem_id
    var cache = {};

    // We use `ajaxComplete' to know if the Ajax call is still working
    // because the logic below to check for cache membership isn't
    // event-driven, so we can't use regular jQuery event callbacks
    var ajaxComplete = false;

    // If we display the `Loading ...' HTML placeholder, we need to
    // know with which newsitem id cluster to replace it with once the
    // Ajax request is done
    var idsToReplaceLoading = null;

    var getHtmlById = function(id) {
        var payload = cache[id];
        return OpenLayers.String.format(popupHtmlTpl, {
            newsitem_id: id,
            html: payload.html,
            schema: payload.schema
        });
    };

    var fetchContent = function(ids) {
        // Fetch the remote HTML and cache it
        $j.ajax({
            type: "GET",
            url: url,
            data: {q: ids.join(",")},
            dataType: "json",
            beforeSend: function() {
                ajaxComplete = false;
            },
            success: function(data) {
                for (var i=0; i < data.length; i++) {
                    var item = data[i];
                    var key = item[0];
                    var html = item[1];
                    var schema = item[2];
                    cache[key] = {html: html, schema: schema};
                }
                ajaxComplete = true;
            },
            complete: function() {
                var htmlList = [];
                for (var i=0; i < idsToReplaceLoading.length; i++) {
                    var id = idsToReplaceLoading[i];
                    htmlList.push(getHtmlById(id));
                }
                var popup = map.currentPopup();
                popup.updateHtml(htmlList);
                popup.addPagination();
            }
        });
    };

    // The real work is done here.
    return eb.lazyFn(function() {
        fetchContent(idsToFetch);

        return function(ids) {
            var htmlList = [];
            for (var i=0; i < ids.length; i++) {
                var id = ids[i];
                // If the Ajax call hasn't finished, the cache won't be
                // populated yet, so return some `Loading ...' markup
                if (!(id in cache) && !ajaxComplete) {
                    idsToReplaceLoading = ids;
                    return [loadingHtml];
                }
                htmlList.push(getHtmlById(id));
            }
            return htmlList;
        };
    });
};

/*
 * Simple wrapper around the _trackEvent() Ajax call on the Google
 * Analytics code, so that we can call it in environments where that
 * code isn't loaded (dev and debug, for instance)
 */
eb.GATrackEvent = function(category, action, label, value) {
    // Check for the existence of the loaded Google Analytics code
    if ("pageTracker" in window && "_trackEvent" in window.pageTracker) {
        window.pageTracker._trackEvent(category, action, label, value);
    } else if ("console" in window) {
        console.log("_trackEvent(\"%s\", \"%s\", \"%s\", %o)", category, action, label, value);
    }
};

// Suppress error message to browsers that lack a vector
// renderer (like Safari 2)
OpenLayers.Layer.Vector.prototype.reportError = false;

OpenLayers.Layer.Vector.prototype.isSupported = function() {
    return this.renderer.supported();
};

window.EveryBlock = window.eb = eb;