
/*
* MapBuilder Integrator 2.1.0
* Copyright(c) 2006-2007, Mashup Technologies, LLC.
* http://mashuptechnologies.com
*/


//Register app.se32bj namespace
Ext.namespace('MapObject.app.CM');

/*
// Redefine after geocoding event
MapObject.widget.GeoCodingPanel.prototype.aftergeocoding = function(lat, lng, zoom) {
	if (this.mapProxy !== null && (typeof this.mapProxy.setCenter == 'function')) {
		// Get max zoom level for a given map type
		zoom = this.mapProxy.mapObject.getCurrentMapType().getMaximumResolution() - 2 ;

		// Set center
		this.mapProxy.setCenter(lat, lng, zoom, this.mapProxy.mapObject.getCurrentMapType());

		// Show center pushpin
		if (this.opt && this.opt.showCenterIcon) {
			// Remove previous marker
			if(this.centerMarker !== null)
			{
				this.mapProxy.removeOverlay(this.centerMarker);	
			}
			// Set center marker
			this.centerMarker = this.mapProxy.addOverlay(lat, lng, {
				'title': this.inputEl.value,
				'icon': this.centerIcon
			});	
		}
	}
};
*/

//
// DataStore
//

/* 
* Data store on load callback function
*/
MapObject.widget.MapViewer.prototype.dsLoad = function (ds) {
	// Center map
	var mapData = this.ds.reader.jsonData.map;

	// Make sure mape has been loaded before we add any markers
	var map = this.mapViewer.getMapProxy().mapObject;
	if (!map.isLoaded()){
		if (this.autoCenter) {
			this.mapViewer.defaultZoom = parseInt(mapData.zoom);
			this.mapViewer.getMapProxy().setCenter(parseFloat(mapData.lat), parseFloat(mapData.lng), this.mapViewer.defaultZoom); 
		}
		else {
			// run setCenter method
			this.setCenter();
		}
	}
			
	// Build icons
	if (this.ds.reader.jsonData.icons) {
		this.mapViewer.buildIcons(this.ds.reader.jsonData.icons);
	}

	// Hide details
	this.recordViewer && this.recordViewer.hide();

	//Clear map - check if map is actually loaded
	if (this.mapViewer.getMapProxy().mapObject != null) {
		this.mapViewer.getMapProxy().clearOverlays();
	}


	/////////////////////////////////////////////////////
	// Display boundaries
	var polygons = [] // storage for GPolygon
	var mo = this.getViewer().getMapProxy().mapObject;
	// get a polygon from a data source
	var Boundary = ds.reader.jsonData.boundaries || false;
	if ( Boundary && (Boundary.length > 0)) {

		var opt =  {
			strokeColor: "#1613A2",
			strokeWeight:1, 
			strokeOpacity:1, 
			fillColor:"#265EB9", 
			fillOpacity: 0.3
		};
		// Access GMap 
		for (var i=0, len=Boundary.length; i<len; i++) {

			var polyPoints = [];

			// 3. Loop through individual locations
			var ring = Boundary[i];
			for (var j=0, len2=ring.length; j<len2; j++) {
				polyPoints.push(new GLatLng(ring[j][0], ring[j][1]));
			}
					
			// create a polygon
			var polygon = new GPolygon(
				polyPoints, 
				opt.strokeColor, 
				opt.strokeWeight, 
				opt.strokeOpacity, 
				opt.fillColor, 
				opt.fillOpacity);
			// Set custom property - save fillColor
			polygon.fillColor = opt.fillColor;

			polygons.push(polygon);

			mo.addOverlay(polygon);
		}
	}
	// EOF Display boundaries

	function processDetails(record)
	{
		// Add locations to a map
		var id = record.get('id');
		var lat = record.get('lat');
		var lng = record.get('lng');

		/* Temp disabled */
		// Check if marker belongs to boundaries
		if (polygons.length == 0) {
			var belongs = true;
		}
		else {
			var belongs = false;
			for (var i=0, len=polygons.length; i<len; i++) {
				if (polygons[i].Contains(new GLatLng(lat, lng))) {
					belongs = true;
					break; // finish - point belongs
				}
			}
		}

		//var belongs = true;
		// Add marker
		if (belongs) {
			var marker = this.mapViewer.getMapProxy().addMarker(id, lat, lng, this.mapViewer.iconsSet[record.get('icon')]);

			// Calculate boundaries 
			bounds.extend(marker.getLatLng());
			newIDs.push(id);
			records_found++;
		}
		else {
			// Remove record from a DS
			records_remove.push(record)
		}
	}


	var newIDs = [];

	// Calculate boundaries 
	var bounds = new GLatLngBounds();

	var records_found = 0; // Counter
	var records_remove = []; // records to be removed from a data store
	// Process data store records
	this.ds.each(processDetails, this);

	for (var i=0, len=records_remove.length; i<len; i++) {
		this.ds.remove(records_remove[i]);
	}

	document.getElementById("recordsCount").innerHTML = "Records found: " + records_found;


	/*
	* Code block loop calculates intersection betwenn already loaded markers and new ones 
	* and removes markers which are outside of intersection scope (they are not loaded).
	*/
	/*
	var markers = this.mapViewer.getMapProxy().gmarkers;
	for(var key in markers){
		if(typeof markers[key] != "function"){
			var found = false;
			for (var i=0, len=newIDs.length; i < len; i++) {
				// entry fpund
				if (key == newIDs[i]) {
					found = true;
					break;
				}
			}
			if (!found) {
				// Check if marker has been already added
				this.mapViewer.getMapProxy().removeOverlay(markers[key]);
				//markers.remove(markers[key]);
				markers[key] = null;
			}
		}
	}
	*/

	// Calculate boundaries 
	if (this.calculateCenter && !bounds.isEmpty()) {
		var center = bounds.getCenter();
		var zoom = map.getBoundsZoomLevel(bounds);
		map.setCenter(center, zoom);
	}
	else 
	// Set center from a data feed
	if (this.autoCenter) {
		this.mapViewer.defaultZoom = parseInt(mapData.zoom);
		this.mapViewer.getMapProxy().setCenter(parseFloat(mapData.lat), parseFloat(mapData.lng), this.mapViewer.defaultZoom); 
	}
	else {
		// run setCenter method
		this.setCenter();
	}

	// Run a hook method
	this.postDSLoadHook(ds);
				
	// Save map position
	this.mapViewer.getMapProxy().mapObject.savePosition();

	/* Use overlay instead 
	* //Create new marker manager
	* var mgr = new GMarkerManager(this.mapViewer.getMapProxy().mapObject);
	* mgr.addMarkers(batch, '9');
	* mgr.refresh();
	*/

};

//MapObject.widget.MapViewer.prototype.postDSLoadHook  = function (ds) {
//
//};

/**
* onMarker click event. It selects grid row and fires rowclick event.
* @param {mixed} marker Record id.
* @return none
*/
MapObject.widget.MapViewer.prototype.onMarkerClick = function (marker) {

		//Select a matching record on a grid
		var index = this.ds.indexOfId(marker.id);
		// Pass second argument to keep existing selection
		this.grid.getSelectionModel().selectRow(index, this.ctrlKey);

		//Expand parent row
		this.grid.plugins.expandRow(index);

		// Do not fire rowclick event here
		//this.grid.fireEvent('rowclick');

		// Scroll selected row into view
		// Scroll selected row into view
		var view = this.grid.view;
		var row = view.getRow(index);
		var top = row.offsetTop;
		view.scroller.dom.scrollTop = parseInt(top);

		// Find active record
		record = this.ds.getById(marker.id);
		// show InfoWindow
		var name = record.get('cname');
		var address = record.get('caddress');
		var description = record.get('cdescription');
		var calc_date = record.get('calc_date');
		var user = record.get('user');
		var link = '<br/><a target="_blank" href="/crime/' + record.get('sid') + '" title="View Crime Details">View Details</a>' ;
		var updatelink = '';

		// Add 'User submitted' prefix
		if (user != '') {
			name = 'User Submitted<br />' + name;
		}

		if (this.showEditLink) {
			updatelink = '<br/><a target="_blank" href="/admin/crime-edit.php?return=' + encodeURIComponent(window.location.href) + '&crime_id=' + record.get('id') + '" title="Update Crime Details">Update Details</a>' ;
		}
		var html = '<div class="tooltip" style="width:250px"><b>' + name + '</b> ' + calc_date +' <br/>' + 
			address + '<div style="height:90px; overflow:auto;">' + description + '</div>' +
			link + updatelink + '</div>';
        this.mapViewer.getMapProxy().mapObject.openInfoWindowHtml(marker.getLatLng(), html);
};

/**
 * tooltipTemplate component designed to fetch and render a tooltip block for the individual datasource record.
 * @namespace MapObject.app.vc
 * @class tooltipTemplate
 * @constructor
 * @param {Ext.data.Store} The instance of data source store object.
 */
MapObject.app.CM.tooltipTemplate = function(ds)
{
	this.init(ds);
}
MapObject.app.CM.tooltipTemplate.prototype = {

	/**
	* Data store
	* @private
	* @type {Ext.data.Store}
	*/
	ds: null,

	/**
	* Tooltip template
	* @private
	* @type {Ext.Template}
	*/
	template: null,

    // Constructor
    init : function(ds){

    	this.ds = ds;// take with Ext.data.Store in {}
		this.template = new Ext.Template(
			'<div class="tooltip">' +
			'<div class="tooltip-container">' +
			'<h2>{prefix}{type}, {address}</h2>' +
			'{time}<br/>' +
			'{description}<br/>' +
			'</div></div>'											
		);

		this.template.compile();

    },

	/**
	* Show a tooltip for a given record id
	* @param {String} record id
	* @return none
	*/
    getHTML: function(id) {

    	var record = this.ds.getById(id);
    	var description = record.get('cdescription');
    	var user = record.get('user');
		// Add 'User submitted' prefix
		var prefix = '';
		if (user != '') {
			prefix = 'User Submitted - ';
		}
        return this.template.apply(
        	{
        		prefix: prefix,
				type: record.get('cname'), 
				address: record.get('caddress'), 
				time: record.get('calc_date') + " " + record.get('calc_time'),
				description: ((description.length > 160) ? (Ext.util.Format.ellipsis(description, 160) + " <b>(click to read more)</b>") : description)
			}                                                                                          
		);
    },

	/**
	* Hide the tooltip block.
	* @param none
	* @return none
	*/
    hide: function () {
    	this.template.hide();
    },

	// No more trailing commas issues
	EOF:null
}


MapObject.app.CM.map = function(opt) {
	this.init(opt);
}

MapObject.app.CM.map.prototype = {
	
	/**
	* City/area name
	* @private
	* @type {string} 
	*/
	area_id: null,

	/**
	* Area configuration options.
	* @private
	* @type {string} 
	*/
	areaConfig: null,

	/**
	* Instance of Gmap2
	* @private
	* @type {Gmap2} 
	*/
	map: null,

	/**
	* Data store
	* @private
	* @type {Ext.data.Store}
	*/
	ds: null,
	
	/**
	* Grid
	* @private
	* @type {Ext.grid.Grid}
	*/
	grid: null,
	
	/**
	* Record Viewer
	* @private
	* @type {MapObject.app.CM.recordViewer}
	*/
	recordViewer: null,

	/**
	* Record Viewer
	* @private
	* @type {MapObject.app.CM.tooltipTemplate}
	*/
	tooltipTemplate: null,

	/**
	* Icons
	* @private
	* @type {Array}
	*/
	icons: null,

	/**
	* Storage for center marker displayed after geocoding.
	* @private
	* @type {GPoint}
	*/
	centerMarker: null,

	/**
	* Initialize object
	* @param none
	* @return none
	*/
    init : function(opt){

    	// Get config option 
    	this.icons = opt.icons;
    	this.area_id = opt.area_id;
    	this.areaConfig = opt.areaConfig;
    	this.showEditLink = opt.showEditLink;
    	this.isLiveSearchDisabled = opt.isLiveSearchDisabled;
    	this.authenticated = opt.authenticated || false;
    	this.isU = opt.isU || false; // is ucr ?
    	this.staticDomain = opt.staticDomain || 'http://s3.spotcrime.com';
		this.dataDomain = opt.dataDomain  || 'http://spotcrime.com';
		//Flag to use s3 data
		this.useS3 = true;

    	// App init

		// crimeList event handler
		// Attach event to the 'notvisited' checkbox
		Ext.get('crimeList').on('click', function(e) {
			var target = e.getTarget();
			if (this.areaConfig.type == 'University') {
				if (target.nodeName.toLowerCase( ) == 'a') {
					var re = /^search-type-/i;
					if (re.test(target.id))
					{
						var id = target.id.substr("search-type-".length);
						try {
							// Process All selection
							if (id == 'All') {
								// Remove selection from other elements
								elements = Ext.DomQuery.select('a', 'crimeList');
								for (var i=0, len = elements.length; i<len; i++) {
									if (elements[i].id=="search-type-All") continue;
									var temp_id = elements[i].id.substr("search-type-".length);
									// UI change
									elements[i].className = "";
									// Unset search parameter
									Ext.get('ctype[' + temp_id + ']').dom.value = '0';
								}
							}
							else {
								// Unselect All
								Ext.get('search-type-All').dom.className = "";
								Ext.get('ctype[All]').dom.value = '0';
							}

							var searchElement = Ext.get('ctype[' + id + ']');
							// Set search parameter
							if (searchElement.dom.value == '1') {
								// Do not uncheck 'all' option - it will be unchecked by selecting another option
								if (id != 'All') {
									searchElement.dom.value = '0';
									// remove class name
									target.className = "";
								}
							}
							else {
								searchElement.dom.value = '1';
								// Set class name
								target.className = "current";
							}

							// Run search 
							this.evtSearch(e);
						}
						catch(e) {alert(e.message);}
					}
				}
			}
			// City selection
			else {
				if (target.type == 'checkbox') {
					if (target.id == "ctype[All]")
					{
						// Enable all other checkboxes
						elements = Ext.DomQuery.select('input[type="checkbox"]');
						for (var i=0, len = elements.length; i<len; i++) {
							if (elements[i].id=="ctype[All]") continue;
							// Do not process userreported checkbox
							if (elements[i].id=="userreported") continue;
							elements[i].disabled= target.checked;
						}
					}
				}
			}
		}, this);

		var dt = new Date();
		// Add date widgets
		var cdate = new Ext.form.DateField({
			applyTo: 'cdate',
			allowBlank:true
		});
		//cdate.applyTo('cdate');
		if (cdate.getValue() == "") {
			cdate.setValue(dt.add(Date.DAY, -14));
		}

		var cdate1 = new Ext.form.DateField({
			applyTo: 'cdate1',
			allowBlank:true
		});
		//cdate1.applyTo('cdate1');
		if (cdate1.getValue() == "") {
			cdate1.setValue(dt);
		}

        //
		// Init map
		//
		/*
		this.map = new GMap2(document.getElementById("map"));
		this.map.addControl(new GSmallMapControl());
		this.map.addControl(new GMapTypeControl());
		// Set map center
		this.map.setCenter(new GLatLng(this.areaConfig.mapCenter.point.lat,this.areaConfig.mapCenter.point.lng), this.areaConfig.mapCenter.zoom);
		*/

		// Set some configuration options
		MapObject.widget.MapViewer.prototype.areaConfig = this.areaConfig;
		MapObject.widget.MapViewer.prototype.showEditLink = this.showEditLink;
		MapObject.widget.MapViewer.prototype.isU = this.isU;
		MapObject.widget.MapViewer.prototype.setCenter = function() {
			if (this.mapInitialized) return;
			this.mapViewer.getMapProxy().setCenter(parseFloat(this.areaConfig.mapCenter.point.lat), parseFloat(this.areaConfig.mapCenter.point.lng), parseInt(this.areaConfig.mapCenter.zoom), (this.isU ? G_NORMAL_MAP : G_NORMAL_MAP));
			this.mapInitialized = true;
		}

    	//
    	// Data Store
    	//

    	if (this.useS3) {
			var pxy = new Ext.data.ScriptTagProxy({
				url: this.dataDomain + '/feed/search.php',
				nocache: true
			});
    	}
    	else {
			// Use XML HTML Request
			var pxy = new Ext.data.HttpProxy({
				url: this.dataDomain + '/feed/search.php',
				nocache: true
			});
    	}
    	// Define the Data Store
		this.ds = new Ext.data.Store({
			// load using script tags for cross domain, if the data in on the same domain as
			// this page, an HttpProxy would be better
			proxy: pxy,
			/* Search Parameters */
	        baseParams: {area_id:this.area_id},

			// create reader that reads the markers
			reader: new Ext.data.JsonReader({
				root: 'crimeList',
				/* totalProperty: 'totalCount', */
				id: 'cdid'
			}, [
				// Make sure id is present as a part of record
				/* Sample record:
				* {"cdid":"336","cdate":"2007-09-10","calc_time":"07:30 PM","clatitude":"39.286679","clongitude":"-76.591440",
				* "cdescription":"","caddress":"400 S. Ann St","cname":"Theft","cicon":"theft.png"}
				*/
				{name: 'id', mapping: 'cdid'},
				{name: 'cid', mapping: 'cid'},
				{name: 'sid', mapping: 'sid'}, // id-hash
				{name: 'lat', mapping: 'clatitude', type: 'float'},
				{name: 'lng', mapping: 'clongitude', type: 'float'},
				{name: 'cdescription', mapping: 'cdescription'},
				{name: 'caddress', mapping: 'caddress'},
				{name: 'cname', mapping: 'cname'},
				{name: 'icon', mapping: 'icon'},
				{name: 'calc_date', mapping: 'calc_date'},
				{name: 'calc_time', mapping: 'calc_time'},
				{name: 'user', mapping: 'user'},
				{name: 'type', mapping: 'type'}
			])
		});


		/* 
		* beforeload Event - Fires before a request is made for a new data object. 
		* If the beforeload handler returns false the load action will be canceled. 
		* 
		* Main responsibility of this hook is to pass search criteria to the server.
		* searchQuery: fieldRep=Select...&address=&zip=&census=&date1=&date2=&owner=Select...&employer=Select...&focus=Select...&ADF_Gap=&construction=Select...
		*/
		function beforeload(options) {
			
			// Flag which indicates that the request to load data has been send
			if (this.requestSent) return false;


			if (!this.dataInitialized) {
				var fileName = this.area_id;
				fileName.toLowerCase();
				var re = /\//i;
				fileName = fileName.replace(re, "-");
				var re = /'/i;
				fileName = fileName.replace(re, "_");
				var re = /"/i;
				fileName = fileName.replace(re, "_");

		    	if (this.useS3) {
					// S3 Cache
					this.ds.proxy.url = this.staticDomain + '/cache/s3/main/2.01/' + fileName + '.js';
		    	}
		    	else {
					// SpotCrime cache used with Ext.data.HttpProxy
					this.ds.proxy.conn.url = '/cache/main/' + fileName + '.js';
				}
			}
			else {
		    	if (this.useS3) {
					this.ds.proxy.url = this.dataDomain + '/feed/search.php'; // Use this with ScriptTagProxy
				}
				else {
					this.ds.proxy.conn.url = '/feed/search.php'; // Use this with HttpProxy
				}
			} 
			
			//Serialize form and send parameters to a server
			options.baseParams.searchQuery = Ext.Ajax.serializeForm('searchForm');
			
			options.baseParams.zoom = '';
			options.baseParams.reloadZoom = '';
			options.baseParams.ne_lat = '';
			options.baseParams.ne_lng = '';
			options.baseParams.sw_lat = '';
			options.baseParams.sw_lng = '';
			if (this.mapViewer) {
				var mo = this.mapViewer.getViewer().getMapProxy().mapObject;
				// Map zoom
				options.baseParams.zoom = mo.getZoom();
				options.baseParams.reloadZoom = this.getRelaodZoom();
				
				// Should we reaload?
				/*dev 
				if ((mo.getZoom() < this.getRelaodZoom()) && this.stopReloadData) {
					return false;
				}
				*/

				var bounds = mo.getBounds();
				// north-east and south-west corners
				options.baseParams.ne_lat = bounds.getNorthEast().lat();
				options.baseParams.ne_lng = bounds.getNorthEast().lng();
				options.baseParams.sw_lat = bounds.getSouthWest().lat();
				options.baseParams.sw_lng = bounds.getSouthWest().lng();
			}
			// Flag which indicates that the request to load data has been send
			this.requestSent = true;

			return true;
		}
		this.ds.on('beforeload', beforeload, this);

		function loadDetails(ds) {
			document.getElementById("recordsCount").innerHTML = "Records found: " + ds.getCount();

			function processDetails(record)
			{
				// Add locations to a map
				var id = record.get('id');
				var lat = record.get('lat');
				var lng = record.get('lng');
				var name = record.get('cname');
				var address = record.get('caddress');
				var type_id = record.get('cid');
				var icon_id = record.get('icon');
				var description = record.get('cdescription');
				var calc_date = record.get('calc_date');
				var user = record.get('user');

				// Add marker
				var point = new GLatLng(lat,lng);
				//var marker = createMarker(point, name, address, type_id);


				if (this.icons[icon_id]) {
					var marker = new GMarker(point, this.icons[icon_id]);
				}
				else {
					var marker = new GMarker(point);
				}
				// Add 'User submitted' prefix
				if (user != '') {
					name = 'User Submitted<br />' + name;
				}
				var html = '<div style="width:250px"><b>' + name + '</b> ' + calc_date +' <br/>' + address + '<div style="height:90px; overflow:auto;">' + description + '</div></div>';
				GEvent.addListener(marker, 'click', function() {
					marker.openInfoWindowHtml(html);
				});


				//var marker = new GMarker(point);
				this.map.addOverlay(marker);
			}

			// Process data store records
			/* avb: processDetails is useless here.
			* To display popup info onMarkerClick has been used.
			* ds.each(processDetails, this);
			*/
		}
		/* Store.load via HttpProxy is async process - the result is not available immediately after the stmt executes.
		* We should use onLoad handler
		*/
		//ab this.ds.on('load', loadDetails, this);

		this.ds.on('load', function(ds) {
			// Moved tp a parent method document.getElementById("recordsCount").innerHTML = "Records found: " + ds.getCount();
			// Show errors
			if (this.dataInitialized && ds.reader.jsonData.errors && ds.reader.jsonData.errors.length > 0) {
				// Flag which indicates that the request to load data has been send. 
				// Set it to false to allow another request.
				this.requestSent = false;

				alert(ds.reader.jsonData.errors.join("\n"));
		    }


		    // Set dataInitialized flag to true.
		    this.dataInitialized = true;

			// Flag which indicates that the request to load data has been send. 
			// Set it to false to allow another request.
			this.requestSent = false;



			/* dev
			var mo = this.mapViewer.getViewer().getMapProxy().mapObject;
			if (mo.getZoom() < this.getRelaodZoom()) {
				this.stopReloadData = true;
			}
			*/

		}, this);


		function createMarker(point, name, address, icon_id) {
			if (this.icons[icon_id]) {
				var marker = new GMarker(point, this.icons[icon_id]);
			}
			else {
				var marker = new GMarker(point);
			}
			var html = "<b>" + name + "</b> <br/>" + address;
			GEvent.addListener(marker, 'click', function() {
				marker.openInfoWindowHtml(html);
			});
			return marker;
		}

    	//
    	// Grid
    	//

		// row expander
		var expander = new Ext.grid.RowExpander({
			tpl : new Ext.Template(
				'{cdescription}' +
				' <a target="_blank" href="/crime/{sid}" title="View Crime Details">View Details</a>' + 
				(this.showEditLink ? ' <a target="_blank" href="/admin/crime-edit.php?crime_id={id}" title="Update Crime Details">Update Details</a>': '')
			)
		});

		// RowSelectionModel
		var sm = new Ext.grid.RowSelectionModel({singleSelect:true});

		// the DefaultColumnModel expects this blob to define columns. It can be extended to provide
        // custom or reusable ColumnModels
        var colModel = new Ext.grid.ColumnModel([
        	expander,
			{header: "", width: 30, sortable: false, dataIndex: 'id', renderer: actionColumnRenderer.createDelegate(this)},
			{header: "Date", width: 90, sortable: true, dataIndex: 'calc_date'},
			{header: "Time", width: 80, sortable: false, renderer: timeRenderer, dataIndex: 'calc_time'},
			{header: "Crime Type", width: 90, sortable: true, dataIndex: 'cname'},
			{header: "Address", width: 380, sortable: true, dataIndex: 'caddress'}
		]);

		// Custom renderer function to render a 'view details' link
		function actionColumnRenderer(v, p, record){
			return '<a target="_blank" title="View Details" href="/crime/' + record.get('sid') + '"><img src="http://s3.spotcrime.com/media/images/spotcrime-magnifier.png" alt="View Details"/></a>';
		}

		// Time custom renderer function
		function timeRenderer(val){
			// String.prototype.trim, defined in ext
			val = val.trim();

			if((val == '') || (val == ':')){
				return '';
			}
			return val;
		}

        // create the Grid
        this.grid = new Ext.grid.GridPanel({
        	renderTo: 'grid-elem',
            store: this.ds,
            cm: colModel,
	        sm: sm,
			loadMask: false,
			//width:(this.isU ? 900 : 740),
			width:(this.isU ? 729 : 740),
			height:400,
            //autoExpandColumn: 'cdescription',
			viewConfig: {
				forceFit:false
			},
			plugins: expander,
			collapsible: true,
	        animCollapse: false,
	        enableColumnHide: false,
			EOF: null
        });


        // Define rowclick event to hide bioPanel content
		this.grid.on('rowclick', 
			function(gridRef, rowIndex, e){ 

			var target = e.getTarget();
			// Check if user clicked on a '+' or '-' 
	        if((target.className != 'x-grid3-row-expander') && (target.className != 'x-grid3-td-expander')){
				// Expand row
				this.grid.plugins.toggleToExpandRow(rowIndex);
			}
		}, this);

        /* DO NOT Re-define row css classes 
        this.grid.getView().getRowClass = function(record, index){
        	return (record.data.type == 'U') ? 'ucrime-row' : 'spotcrime-row';
		};
		*/

        // Record viewer
        //this.recordViewer = new MapObject.app.CM.recordViewer(this.ds);

        // Record viewer
        this.tooltipTemplate = new MapObject.app.CM.tooltipTemplate(this.ds);

		// Instantiate MapViewer class
		this.mapViewer = new MapObject.widget.MapViewer( {
			'ds': this.ds,
			'grid': this.grid,
			'recordViewer': this.recordViewer,
			'tooltipTemplate': this.tooltipTemplate,
			'gridHelp' : null,
			'autoCenter' : false, 
			'focusGridRow' : false,
			'mapConfig' : {
				'container' : 'map',
				'tooltipEngine' : 'v2',
				'mapControls' : {largeMapControl: true},
				'markersEngine' : 'MarkerManager',
				'icons' : this.icons,
				'mapTypes' : {
					normal: true,
					satellite: true,
					hybrid: true,
					physical: true
				},
				defaultMapType: (this.isU ? 'Normal' : 'Normal')
			}
		});

		// Access GMap 
		var mo = this.mapViewer.getViewer().getMapProxy().mapObject;
		// Event to load polygons for a given map boundaries  only
		GEvent.addListener(mo, "dragend", function() { // use moveend to catch automatic map movement (e.g. pop up recenters map)
			if (this.isLiveSearchDisabled) { // Is search disabled?
				return;
			}

			if (this.stopReloadData != undefined) {
				this.ds.load();
			}
		}.createDelegate(this));

		// Event fired when zoom changes. Also zoomend is fired on a map load.
		GEvent.addListener(mo, "zoomend", function(oldzoom, newzoom) {
			if (this.isLiveSearchDisabled) { // Is search disabled?
				return;
			}

			// (oldzoom == undefined) means that zoom is set on the initial map load

			
			// Put a flag to start reloading
			/*
			if ((oldzoom >= this.getRelaodZoom()) && (newzoom < this.getRelaodZoom())) {
				this.stopReloadData = false;
			}
			else if ((oldzoom < this.getRelaodZoom()) && (newzoom >= this.getRelaodZoom())) {
				this.stopReloadData = false;
			}
			*/

			// Do not reload data after an initial map load
			if (oldzoom != undefined) {
				this.ds.load();
				this.stopReloadData = false;
			}
			
		}.createDelegate(this));

        //ab this.grid.render();
		
        if (!this.isU) {
			// Insert Google ads. Do not use ads for a ucrime
			adsManager = new GAdsManager(mo, "ca-pub-7922093619579857", 
				{
					channel: '7208850964',
					maxAdsOnMap : 2,
					style: 'adunit'
				}		
			);
			adsManager.enable();
		}

		var geocoder = new MapObject.proxy.GoogleGeocoder();
		// Instantiate geocoding widget
		var geocodingPanel = new MapObject.widget.GeoCodingPanel(
			'geocodeAddressInput',
			'geocodeButton',
		    geocoder,
		    this.mapViewer.getViewer('Google'),
			{
				showCenterIcon: true, 
				maxZoom: true
			}
		)

		geocodingPanel.on("beforegeocoding", function() {
			var inputEl = document.getElementById('geocodeAddressInput');
			var addr = Ext.util.Format.trim(inputEl.value.toLowerCase());

			// Verify if we need to add a city here
			var addCity = false;
			var isZip = false;
            var reZip = /^\d{5}([\-]\d{4})?$/;
            var addressSearch = this.areaConfig.city.toLowerCase();
			var done = false;

			// Split an address using ,
			var addressChunks = addr.split(",");
			for (var i=0, len=addressChunks.length; i< len; i++) {
            	isZip = reZip.test(Ext.util.Format.trim(addressChunks[i]));
            	if (isZip) {
					addCity = false;
					done = true;
					break;
				}
			}

			//1. Check if city name is present
			if (!done) {
				addCity = (addr.indexOf(addressSearch) === -1) ;
			}

            //2. Check if zip was entered
            if (!done && !isZip) {
            	isZip = reZip.test(addr);
            }

			if (addCity) {
				inputEl.value = inputEl.value + ", " + this.areaConfig.city + ", " + this.areaConfig.state;
			}
		}, this);



		//
		// Register application events for misc. HTML elements
		//

		// Attach event to the 'search' button
		var showBtn = Ext.get('btnSearch');
		if (showBtn) {
			// Attach event to the 'search' button
			showBtn.on('click', this.evtSearch, this);
		}

		// Add filtering handlers
		//Ext.get('mapLegend').on('click', this.filterByLegend, this);
		
        // Finally load data
        this.dataInitialized = false; // flag to prevent from errors output
        //this.ds.load();

        // Init ads manager. Do not use ads for a ucrime
        /* Disable lat49 
        if (!this.isU) {
			var sysAd = new sysAdsManager(this.mapViewer.getViewer().getMapProxy().mapObject, {lat49:{enabled: true}, isU: this.isU});
		}
		*/
    },
     
	/**
	* search
	* @param e Event handler
	* @return none
	*/
	evtSearch: function(e) {
		if (this.isLiveSearchDisabled) { // Is search disabled?
			alert('Due to high traffic our Crime Search is not working at the moment. Please signup for our crime alerts for updated crime information.');

			return;
		}


		if (!this.authenticated) {
			var old_OK = Ext.MessageBox.buttonText.ok;
			var old_Cancel = Ext.MessageBox.buttonText.cancel;
			Ext.MessageBox.buttonText.ok = 'Yes, I\'d like to signup for crime alerts now';
			Ext.MessageBox.buttonText.cancel = 'Not interested';

			Ext.Msg.show({
				title:'',
				msg: 'Our advanced crime search is available for subscribed users only. Please signup now! It\'s free!',
				width: 400,
				buttons: Ext.Msg.OKCANCEL,
				fn: function(btn, text) {if(btn == 'ok') {window.location.href = '/tv';} },
				animEl: 'signup-box-large',
				icon: Ext.MessageBox.INFO
			});

			Ext.MessageBox.buttonText.ok = old_OK;
			Ext.MessageBox.buttonText.cancel = old_Cancel;
		}
		else {

			this.stopReloadData = false;
		
			//Remove all Records from the Store and fires the clear event.
			this.cleanUp();
		
			// Invoke search
			this.ds.load();

		}
	},

	/**
	* Clean data and markers
	* @param none
	* @return none
	*/
	cleanUp: function() {
		//Remove all Records from the Store and fires the clear event.
		this.ds.removeAll();
		
		//Remove all icons and overlays.
		//This fixes active icon mouse-over issue after data reloading
		if (this.mapViewer.getViewer().getMapProxy().mapObject != null) {
			var mo = this.mapViewer.getViewer().getMapProxy();
			mo.clearOverlays();
			mo.gmarkers = [];
			mo.selectedMarkersIDs = [];
		}
	},

	/**
	* Get min zoom level to load data dynamically
	* @param none
	* @return none
	*/
	getRelaodZoom: function() {
		//return 16;
		// Find max zoom level based on selected map type
		var mo = this.mapViewer.getViewer().getMapProxy().mapObject;
		return (mo.getCurrentMapType() ? (mo.getCurrentMapType().getMaximumResolution() - 2) : '16');
		//return this.isU ? 14 : 16;
		//return 0;
	}

}

/* 
* Object literal to manage application popup windows.
*/
MapObject.app.CM.popupManager = {

	/**
	* Storage for the "Tips" window object
	* @private
	* @type {Ext.Window}
	*/
	tipsWin: null,
	/**
	* Storage for the "feedback" window object
	* @private
	* @type {Ext.Window}
	*/
	showFeedback: null,

	/**
	* Init popups - add event listeners
	* @param none
	* @return none
	*/
	init: function() {
		// Add filtering handlers
		if (Ext.get('inlineList')) {
			Ext.get('inlineList').on('click', this.showPopup, this);
		}

		if (Ext.get('crime-classifications')) {
			Ext.get('crime-classifications').on('click', this.showPopup, this);
		}
	},

	/**
	* Event handler to show popups.
	* @param {Ext.EventObject} e
	* @return none
	*/
	showPopup: function(e) {
		var target = e.getTarget();

		if ((target.id == "showTips") || (target.id == "crime-classifications")) {
			this.showTips();
		}
		else if ((target.id == "showFeedback")) {
			this.showFeedback();
		}
		
	},
	/**
	* Show 'Tips for Best Use' popup
	* @param none
	* @return none
	*/
	showTips: function() {
        // create the window on the first click and reuse on subsequent clicks
        if(!this.tipsWin){
            this.tipsWin = new Ext.Window({
                el:'tipsBestView',
                layout:'fit',
                autoScroll: true,
                width:700,
                height:500,
                closeAction:'hide',
                plain: true,
                modal: false,
                resizable: false,
                
                contentEl: 'tipsPanel',

                buttons: [{
                    text: 'Close',
                    handler: (function(){
                        this.tipsWin.hide();
                    }).createDelegate(this)
                }]
            });
        }
        this.tipsWin.show(this);

	},


	/**
	* Show a feedback form
	* @param none
	* @return none
	*/
	showFeedback: function() {
        // create the window on the first click and reuse on subsequent clicks
        if(!this.feedbackWin){
            this.feedbackWin = new Ext.Window({
                el:'feedback',
                layout:'fit',
                width:700,
                height:450,
                closeAction:'hide',
                plain: true,
                modal: true,
                resizable: false,
                
                contentEl: 'feedbackPanel',

                buttons: [{
                    text:'Submit',
                    handler: (function(){
						var feedbackOpt = Ext.Ajax.serializeForm('feedbackForm');

						//Make ajax call and save export parameters in a session
						Ext.Ajax.request({
							url: '/feed/sendFeedback.php',
							success: function (response, options) {
		                        this.feedbackWin.hide();
		                        //Remove feddback text
		                        Ext.get("feedback").dom.value = "";
						        Ext.MessageBox.alert('Feedback Status', 'Your feedback has been sent.');
							},
							failure: function () {alert("Feedback failed. Please try again.")},
							scope: this,
							params: {
								'op': feedbackOpt
							}
						});

                    }).createDelegate(this)

                },{
                    text: 'Close',
                    handler: (function(){
                        this.feedbackWin.hide();
                    }).createDelegate(this)
                }]
            });
        }
        this.feedbackWin.show(this);

	}

}



/*
 * Ext JS Library 2.0
 * Copyright(c) 2006-2007, Ext JS, LLC.
 * licensing@extjs.com
 * 
 * http://extjs.com/license
 */

Ext.grid.RowExpander = function(config){
    Ext.apply(this, config);

    this.addEvents({
        beforeexpand : true,
        expand: true,
        beforecollapse: true,
        collapse: true
    });

    Ext.grid.RowExpander.superclass.constructor.call(this);

    if(this.tpl){
        if(typeof this.tpl == 'string'){
            this.tpl = new Ext.Template(this.tpl);
        }
        this.tpl.compile();
    }

    this.state = {};
    this.bodyContent = {};
};

Ext.extend(Ext.grid.RowExpander, Ext.util.Observable, {
    header: "",
    width: 20,
    sortable: false,
    fixed:true,
    dataIndex: '',
    id: 'expander',
    lazyRender : true,
    enableCaching: true,

    getRowClass : function(record, rowIndex, p, ds){
        p.cols = p.cols-1;
        var content = this.bodyContent[record.id];
        if(!content && !this.lazyRender){
            content = this.getBodyContent(record, rowIndex);
        }
        if(content){
            p.body = content;
        }
        return this.state[record.id] ? 'x-grid3-row-expanded' : 'x-grid3-row-collapsed';
    },

    init : function(grid){
        this.grid = grid;

        var view = grid.getView();
        view.getRowClass = this.getRowClass.createDelegate(this);

        view.enableRowBody = true;

        grid.on('render', function(){
            view.mainBody.on('mousedown', this.onMouseDown, this);
        }, this);
    },

    getBodyContent : function(record, index){
        if(!this.enableCaching){
            return this.tpl.apply(record.data);
        }
        var content = this.bodyContent[record.id];
        if(!content){
            content = this.tpl.apply(record.data);
            this.bodyContent[record.id] = content;
        }
        return content;
    },

    onMouseDown : function(e, t){
        if(t.className == 'x-grid3-row-expander'){
            e.stopEvent();
            var row = e.getTarget('.x-grid3-row');
            this.toggleRow(row);
        }
    },

    renderer : function(v, p, record){
        p.cellAttr = 'rowspan="2"';
        return '<div class="x-grid3-row-expander">&#160;</div>';
    },

    beforeExpand : function(record, body, rowIndex){
        if(this.fireEvent('beforeexpand', this, record, body, rowIndex) !== false){
            if(this.tpl && this.lazyRender){
                body.innerHTML = this.getBodyContent(record, rowIndex);
            }
            return true;
        }else{
            return false;
        }
    },

    toggleRow : function(row){
        if(typeof row == 'number'){
            row = this.grid.view.getRow(row);
        }
        this[Ext.fly(row).hasClass('x-grid3-row-collapsed') ? 'expandRow' : 'collapseRow'](row);
    },

    // Custom method to toggle and expand rows
    toggleToExpandRow : function(row){
        if(typeof row == 'number'){
            row = this.grid.view.getRow(row);
        }
        this[Ext.fly(row).hasClass('x-grid3-row-collapsed') ? 'expandRow' : 'expandRow'](row);
    },

    expandRow : function(row){
        if(typeof row == 'number'){
            row = this.grid.view.getRow(row);
        }
        var record = this.grid.store.getAt(row.rowIndex);
        var body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row);

        if(this.beforeExpand(record, body, row.rowIndex)){
            this.state[record.id] = true;
            Ext.fly(row).replaceClass('x-grid3-row-collapsed', 'x-grid3-row-expanded');
            this.fireEvent('expand', this, record, body, row.rowIndex);


			// My custom logic: Scroll selected row into view
			//This doen't scroll to the top this.grid.view.focusRow(row.rowIndex);
			/*
			var view = this.grid.view;
			var top = row.offsetTop;
	        view.scroller.dom.scrollTop = parseInt(top);
	        */

	        // Select expanded row
	        this.grid.getSelectionModel().selectRow(row.rowIndex);

        }

    },

    collapseRow : function(row){
        if(typeof row == 'number'){
            row = this.grid.view.getRow(row);
        }
        var record = this.grid.store.getAt(row.rowIndex);
        var body = Ext.fly(row).child('tr:nth(1) div.x-grid3-row-body', true);
        if(this.fireEvent('beforcollapse', this, record, body, row.rowIndex) !== false){
            this.state[record.id] = false;
            Ext.fly(row).replaceClass('x-grid3-row-expanded', 'x-grid3-row-collapsed');
            this.fireEvent('collapse', this, record, body, row.rowIndex);
        }
    }
});

/*
* MapBuilder Integrator 2.1.0
* Copyright(c) 2006-2007, Mashup Technologies, LLC.
* http://mashuptechnologies.com
*/
MapObject.app.CM.auth = {
	signupTemplate: null,
	
	/**
	* Storage for the "Signup" window object
	* @private
	* @type {Ext.Window}
	*/
	signupWin: null,
	/**
	* Storage for the "Login" window object
	* @private
	* @type {Ext.Window}
	*/
	loginWin: null,

	/**
	* Storage for the active window object
	* @private
	* @type {Ext.Window}
	*/
	activeWin: null,
	
	/**
	* Storage for the map object
	* @private
	* @type {MapObject.app.CM.map}
	*/
	map: null,

	/**
	* Signup required fields
	* @private
	* @type array
	*/
	signupRequiredFields: [{el:'signup-email'}, {el:'signup-password'}, {el:'signup-confpassword'}, 
		{el:'crime-alert-address'}, {el:'crime-alert-city'}],
	/**
	* Login required fields
	* @private
	* @type array
	*/
	loginRequiredFields: [{el:'login-username'}, {el:'login-password'}],

	/**
	* Init popups - add event listeners
	* @param none
	* @return none
	*/
	init: function() {

		// Register onlick event for the auth elements
		Ext.get('signupLink') && Ext.get('signupLink').on('click', function(e) {
			MapObject.app.CM.auth.showForm("signup");
		});
		/* AJAX popup had been replaced with a static login page
		*Ext.get('loginLink') && Ext.get('loginLink').on('click', function(e) {
		*	MapObject.app.CM.auth.showForm("login");
		*});
		*/
	},
	
	/*
	* Show bio popup for a proffesional with a given id
	*/
	showForm: function(action) {
		// Clear errors
		var elError = Ext.get(action + 'Error');
		elError.dom.innerHTML = '';
		elError.hide();

		if (action ==  "signup") {
			// Content panels
			this.signupPanel = Ext.get('signupPanel');

			// create the window on the first click and reuse on subsequent clicks
			if(!this.signupWin){
				//Setup some client-side validators
				this.signupRequiredFields = this.setRequiredFields(this.signupRequiredFields);

				this.signupWin = new Ext.Window({
					el:'signupWin',
					layout:'fit',
					autoScroll: true,
					width:'auto',
					height:'auto',
					closeAction:'hide',
					plain: true,
					modal: true,
	                resizable: false,
                
					contentEl: 'signupPanel',

					buttons: [
					{
						text:'Submit',
						type: 'submit',
						handler: this.submit.createDelegate(this)
                	},
					{
						text: 'Close',
						handler: (function(){
							this.signupWin.hide();
						}).createDelegate(this)
					}]
				});
				
				// Focus first element
				this.signupWin.addListener("show", function(){
					// Focus first element
					Ext.get('signup-email').focus();
				}, this, {delay:100});

				// Add 'Submit' event handler
				Ext.get('signupForm').on('keydown', function(e) {
					if (e.getKey() == Ext.EventObject.ENTER) {
						this.submit();
					}
				}, this);

			}


			this.activeWin = this.signupWin; 
		}
		// Login
		else {

			// Content panels
			this.loginPanel = Ext.get('loginPanel');

			// create the window on the first click and reuse on subsequent clicks
			if(!this.loginWin){
				//Setup some client-side validators
				this.loginRequiredFields = this.setRequiredFields(this.loginRequiredFields);

				this.loginWin = new Ext.Window({
					el:'loginWin',
					layout:'fit',
					autoScroll: true,
					width:'auto',
					height:'auto',
					closeAction:'hide',
					plain: true,
					modal: true,
	                resizable: false,
                
					contentEl: 'loginPanel',

					buttons: [
					{
						text:'Submit',
						type: 'submit',
						handler: this.submit.createDelegate(this)
                	},
					{
						text: 'Close',
						handler: (function(){
							this.loginWin.hide();
						}).createDelegate(this)
					}]

				});

				this.loginWin.addListener("show", function(){
					// Focus first element
					Ext.get('login-username').focus();
				}, this, {delay:100});


				// Add 'Submit' event handler
				Ext.get('loginForm').on('keydown', function(e) {
					if (e.getKey() == Ext.EventObject.ENTER) {
						this.submit();
					}
				}, this);
			}
			this.activeWin = this.loginWin; 
		}
		this.activeWin.action = action;
		this.activeWin.show();

	},

	submit: function() {
		
		var action = this.activeWin.action;

		var params = {};
		if (action ==  "signup") {
			var valid = this.validateRequiredFields(this.signupRequiredFields);
			if (valid){
				params = Ext.Ajax.serializeForm('signupForm');
			}
			else {
		        //Ext.MessageBox.alert('Signup Error', 'Please enter all required data.');
				// Show a dialog using config options:
				Ext.Msg.show({
					title:'Signup Error',
					msg: 'Please fill-in all required fields.',
					buttons: Ext.Msg.OK,
					icon: Ext.MessageBox.ERROR
				});
			}
		}
		// login
		else {
			var valid = this.validateRequiredFields(this.loginRequiredFields);
			if (valid){
				params = Ext.Ajax.serializeForm('loginForm');
			}
			else {
				// Show a dialog using config options:
				Ext.Msg.show({
					title:'Login Error',
					msg: 'Please fill-in all required fields.',
					buttons: Ext.Msg.OK,
					icon: Ext.MessageBox.ERROR
				});
			}
		}

		if (!valid){
			return;
		}
		//Make ajax call to process authentication request
		Ext.Ajax.request({
			url: '/feed/auth.php',
			params: {
				'action': action,
				'params': params
			},
			scope: this,
			success: (function(response, options) {

	            o = Ext.util.JSON.decode(response.responseText);
				
				/*

				// Details for lease type
				this.signupTemplate.overwrite(this.signupPanel, {
					Name: o.Name,
					Title: o.Title,
					Blog: ((o.Blog == '') ? '' : ('<a href="' + o.Blog + '" target="_blank">Blog</a>')),
					Bio: o.Bio
				});
				this.signupWin.setTitle(o.Name + " Bio");
				*/
				
				var elError = Ext.get(options.params.action + 'Error');
				// Check for errors
				if (o.errors.length > 0) {
					elError.dom.innerHTML = o.errors.join("<br/>") ;
					elError.show();
				}
				// Sucesfull
				else {
					elError.hide();

					// Hide register panel - remove it from DOM
					var elRegister = Ext.get('register');
					elRegister.setVisibilityMode(Ext.Element.DISPLAY);

					var elWelcome = Ext.get('welcome');
					elWelcome.setVisibilityMode(Ext.Element.DISPLAY);

					if (options.params.action == 'signup') {
						// Show thank you message
						elRegister.setVisible(true);
						elWelcome.setVisible(false);

						// Show Thank-you window
				        Ext.MessageBox.alert('Thank You', 'Thanks for registering! Please check your email for an account verification info, and click on the embedded link to confirm your email address. If you encounter any problems, email us at <a href="mailto:support@reportsee.com">support@reportsee.com</a>.');
					}
					// Sign in
					else {

						Ext.get('userName').dom.innerHTML = o.Name;

						//elWelcome.show();
						elWelcome.setVisible(true);

						//elRegister.hide();
						elRegister.setVisible(false);

						// Reload data
						this.reloadData();

					} // else
					// Hide popup
					this.activeWin.hide();

					if (options.params.action == 'signup') {
						// Remove entered passwords
						Ext.get('signup-password').dom.value = '';
						Ext.get('signup-confpassword').dom.value = '';
					}
					else {
						// Remove entered password
						Ext.get('login-password').dom.value = '';

					}
				}

				/*
				if (options.params.action == "signup") {
					this.signupPanel.show();
					this.signupWin.show(this);
				}
				// Login
				else {
					this.loginPanel.show();
					this.loginWin.show(this);
				}
				*/
				

			}).createDelegate(this),
			failure: function () {
				this.activeWin.hide();
				alert("Authentication processing failed. Please try again.")
			}
		});
	},

	logout: function() {
		
		//Make ajax call to log out
		Ext.Ajax.request({
			url: '/feed/logout.php',
			params: {
				'action': 'logout'
			},
			scope: this,
			success: (function(response, options) {

				var elWelcome = Ext.get('welcome');
				elWelcome.setVisibilityMode(Ext.Element.DISPLAY);
				//elWelcome.hide();
				elWelcome.setVisible(false);

				// Show register panel - remove it from DOM
				var elRegister = Ext.get('register');
				if (elRegister) {
					elRegister.setVisibilityMode(Ext.Element.DISPLAY);
					//elRegister.show();
					elRegister.setVisible(true);
				}

				// Reload data
				this.reloadData();

			}).createDelegate(this),
			failure: function () {
				this.activeWin.hide();
				alert("Log out processing failed. Please try again.")
			}
		});
	},

	reloadData: function() {
		return; // Do  not reload data.

		if (this.map === null) return;

		//Remove all Records from the Store and fires the clear event.
		this.map.ds.removeAll();
        // Invoke search
        this.map.ds.load();
	},

	setRequiredFields: function(data) {
		for(var i=0, len=data.length; i<len; i++) {
			data[i].obj = new Ext.form.TextField({
				allowBlank: false,
				applyTo: data[i].el,
				selectOnFocus:true
			});
		}
		return data;
	},
	validateRequiredFields: function(data) {
		var valid = true;
		for(var i=0, len=data.length; i<len; i++) {
			valid = valid && data[i].obj.validate();
		}
		return valid;
	}


}
/* Application Tooltip manager */
MapObject.app.CM.toolTipManager = {

	init: function(elements) {
		for (var i=0, len = elements.length; i<len; i++) {
			new Ext.ToolTip({
				target: elements[i].id,
				title: elements[i].title,
				width:350,
				html: elements[i].html,
				trackMouse:true
			});
		}

		Ext.QuickTips.init();
	}
}

/*
* Crime alerts UI manager
*/
MapObject.app.CM.crimeAlert = {

	initSignupForm: function() {
		// Attach event to the 'notvisited' checkbox
		Ext.get('crime-alerts').on('click', function(e) {
			var target = e.getTarget();
			var parent = null;
			if (target.id == 'crime-alert-cell') {
				if (target.checked) {
					// Enable through CSS
					parent = Ext.get('crime-alert-cell-number').parent('div').parent('div');
					if (parent !== null) parent.removeClass("x-item-disabled");
					parent = Ext.get('crime-alert-cell-carrier').parent('div').parent('div');
					if (parent !== null) parent.removeClass("x-item-disabled");
					parent = Ext.get('crime-alert-cell-frequency').parent('div').parent('div');
					if (parent !== null) parent.removeClass("x-item-disabled");

					// Enable through DOM
					Ext.get('crime-alert-cell-number').dom.disabled = false;
					Ext.get('crime-alert-cell-carrier').dom.disabled = false;
					Ext.get('crime-alert-cell-frequency').dom.disabled = false;
				}
				else {
					// Disable through CSS
					parent = Ext.get('crime-alert-cell-number').parent('div').parent('div');
					if (parent !== null) parent.addClass("x-item-disabled");
					parent = Ext.get('crime-alert-cell-carrier').parent('div').parent('div');
					if (parent !== null) parent.addClass("x-item-disabled");

					// Disable through DOM
					Ext.get('crime-alert-cell-number').dom.disabled = true;
					Ext.get('crime-alert-cell-carrier').dom.disabled = true;
					Ext.get('crime-alert-cell-frequency').dom.disabled = true;
				}
			}
			else
			if (target.id == 'crime-alert-email') {
				if (target.checked) {
					// Enable through CSS
					parent = Ext.get('crime-alert-email-format').parent('div').parent('div');
					if (parent !== null) parent.removeClass("x-item-disabled");
					parent = Ext.get('crime-alert-email-frequency').parent('div').parent('div');
					if (parent !== null) parent.removeClass("x-item-disabled");

					// Enable through DOM
					Ext.get('crime-alert-email-format').dom.disabled = false;
					Ext.get('crime-alert-email-frequency').dom.disabled = false;
				}
				else {
					// Disable through CSS
					parent = Ext.get('crime-alert-email-format').parent('div').parent('div');
					if (parent !== null) parent.addClass("x-item-disabled");
					parent = Ext.get('crime-alert-email-frequency').parent('div').parent('div');
					if (parent !== null) parent.addClass("x-item-disabled");

					// Disable through DOM
					Ext.get('crime-alert-email-format').dom.disabled = true;
					Ext.get('crime-alert-email-frequency').dom.disabled = true;
				}
			}
		});
	}

}

/*
* Map geocoder.
*/
MapObject.app.CM.mapGeocoder = {
	init: function(config) {

		// Instantiate GoogleMap class
		this.mapViewer = new MapObject.proxy.GoogleMap(config.container, (config.mapConfig || {}));

		this.mapViewer.multipleDragableMarkers = false;

		this.mapViewer.on('afterloadmap', function () {
			this.mapViewer.setCenter(config.mapCenter.lat, config.mapCenter.lng, config.mapCenter.zoom);
		}, this);

		// Set lat/lng values on a form
		this.setLatLong = function(lat, lon, accuracy) {
			Ext.get('latitude').dom.value = lat;
			Ext.get('longitude').dom.value = lon;
		};
		this.onGeocode = function(lat, lon, accuracy) {
			this.setLatLong(lat, lon, accuracy);

			this.mapViewer.setCenter(lat, lon, this.geocoder.accuracy[0].levels[accuracy]);

			// Add dragable marker 
			this.mapViewer.addDragableOverlay(lat, lon);
		};
			

		this.mapViewer.onMapClick = this.setLatLong;

		// Load map, e.g. set center
		this.mapViewer.loadMap();

		//Add dragable support
		this.mapViewer.registerMapClick();

		this.geocoder = new MapObject.proxy.GoogleGeocoder();

		this.geocoder.on("aftergeocoding", this.onGeocode, this);

		// Add event listener for geocode button
		Ext.get('GeocodeButton').on('click', function () {
			addr = [
				Ext.get('address').dom.value,
				Ext.get('city').dom.value,
				Ext.get('zip').dom.value
			]
			this.geocoder.geocode(addr.join(" "));
		}, this);

		// Add DragableOverlay if necessary
		if (config.marker && config.marker.lat && config.marker.lng)
		{
			this.mapViewer.addDragableOverlay(config.marker.lat, config.marker.lng);
		}
	}
	
}

// sysAdsManager - class responsible for ads rendering
var sysAdsManager = function (map, opt) {
	// Constructor
	this.init(map, opt);
}
      
sysAdsManager.prototype = {
	map:null,
	adContainer: null, 
	//lat49AdContainer ad container
	lat49AdContainer: null,
	adsRegistry: [], 
	adDomNode: null, 
	// Register 3rd party ad systems
	adSystems: {lat49: true},
	isU: false,
       
    count: 0,

	init:function(map, opt){

		// Register system ads
		this.registerAds();

		this.map = map;
		var isLat49Loaded = (typeof Lat49 !== 'undefined');
		opt = opt || {
			lat49: {enabled : true} 	
		}
		this.adSystems.lat49 = ((isLat49Loaded && opt.lat49.enabled) || false);
		this.isU = opt.isU;
		
		
		/* Create ad container */
		this.adContainer=document.createElement('div');
		this.adContainer.id='adcontainer';
		this.adContainer.style.position='absolute';
		this.adContainer.style.verticalAlign='bottom';
		this.adContainer.style.right='5px';
		this.adContainer.style.bottom='25px';
		this.adContainer.style.width='242px';
		this.adContainer.style.height='133px';
		this.adContainer.style.zIndex=99999;
		this.adContainer.style.backgroundColor='transparent';

		this.map.getContainer().appendChild(this.adContainer);

		if (this.isAdSystemAvailable('lat49')) {
			/* Create lat49 ad container */
			this.lat49AdContainer=document.createElement('div');
			this.lat49AdContainer.id='lat49AdContainer';
			this.lat49AdContainer.setAttribute('lat49adposition', 'bottom-right');
			this.lat49AdContainer.style.position='absolute';
			this.lat49AdContainer.style.verticalAlign='bottom';
			this.lat49AdContainer.style.right='5px';
			this.lat49AdContainer.style.bottom='5px';
			/*I would suggest is that you remove the
			* height & width paramaters from the div you have set up for the lat49 ads. If
			* you leave the height & width parameters in there, you'll find that in FF, if
			* you mouse over the ad area when there is no ad, you will have some strange
			* behavior where the mouse doesn't behave as expected in that region. 
			*
			* this.lat49AdContainer.style.width='242px';
			* this.lat49AdContainer.style.height='133px';
			*/
			this.lat49AdContainer.style.marginBottom='8px';
			this.lat49AdContainer.style.zIndex=99999;
			this.lat49AdContainer.style.backgroundColor='transparent';
			this.map.getContainer().appendChild(this.lat49AdContainer);
			
			// Initialize the Lat49 API with publisher Id
			Lat49.initAds((this.isU ? 479 : 403));

			// Register a helper function to check is ad is available
			Lat49.AdHelper.prototype.isAdAvailable = function(divId) {
				var adDiv = document.getElementById(divId);
				if (adDiv == null)
					return false;
				var available = false;
				var child = adDiv.firstChild;
				while (child) {
					var childID = child.id;
					if (childID && (childID.substring(0,7) == "lat49ad")) {
						// Check if ad is available. Lat49 puts ads inside div with id like 
						// <div id="adcontainer" lat49adposition="bottom-right" style="position: absolute; vertical-align: bottom; right: 5px; bottom: 25px; width: 234px; height: 60px; z-index: 99999; background-color: transparent;">
						if (child.innerHTML != "") {
							available = true;
							break;
						}
					}
					child = child.nextSibling;
				}
				return available;
			}

		}
		// This event is fired when the change of the map view ends.
		GEvent.bind(this.map, "moveend", this, this.adListener);

		// Fire showAd event to display the system ad for initial map state
		this.showAd();
	},

	adListener: function(){
		this.showAd(false);
	},

	showAd:function(){
		var center = this.map.getCenter();
		// center might not be available
		if (!center) return;

		var lat = center.lat();
		var lon = center.lng();
		var zoomlevel = this.map.getZoom();

		// Get ad object
		var ad = this.getAdObject();

		// Remove previous ad
		if (this.adDomNode != null) {
			this.adContainer.removeChild(this.adDomNode);
			this.adDomNode = null;
		}

		// Flag which controld output of system ads
		var internalAd = ad ? true : false;

		// Display internal add only one time
		internalAd = (this.count == 0) ? true : false;
		if (this.isU && internalAd) internalAd = false; // Do not show internal add for ucrime

		if (internalAd){

			this.adDomNode = ad.getDomNode();

			// Get ad width and height	
			this.adContainer.style.width=this.adDomNode.style.width;
			this.adContainer.style.height=this.adDomNode.style.height;

			// Append ad node
			this.adContainer.appendChild(this.adDomNode);

			// Change lat 49 position
			/*
			if (this.isAdSystemAvailable('lat49') && (zoomlevel > 4)) {
				this.lat49AdContainer.style.bottom = (parseInt(this.adContainer.style.height) + 25) + 'px';
			}
			*/
		}

		// Check lat49 ad availability
		//if (this.isAdSystemAvailable('lat49') && (zoomlevel > 6)) {
		if (!internalAd) {

			var zoomlevel = Lat49.Tile.convertGMap2Zoom(this.map.getZoom());

			Lat49.updateAdByLatLon(this.lat49AdContainer.id, lat, lon, zoomlevel);

			/*
			* // Check if lat49 is available (hack)
			* var available = Lat49.getAdHelper().isAdAvailable(this.lat49AdContainer.id);
			* 
			* if (available) {
			* 	internalAd = false;
			* }
			*/
		}

		// Increase ads counter
		this.count++;
	},


	getContainer:function(){
		return this.adContainer;
	},

	registerAds:function(){
		this.adsRegistry.push( new sysAdsManager.Ad(150, 60, "http://ucrime.com", "http://ucrime.com/media/style/ucrime/images/ucrimes_logo-150x60.jpg", "University Crime"));
	},

	getAdObject:function(){
		var randomNum = Math.floor(Math.random() * (this.adsRegistry.length));
		return this.adsRegistry[randomNum];
	},

	/* Check if ad system is available */
	isAdSystemAvailable: function (system) {
		return (this.adSystems[system]) ? true : false;
	}

}

/* Ad class */
sysAdsManager.Ad = function (width, height, url, src, text) {
	// Constructor
	this.init(width, height, url, src, text);
}
sysAdsManager.Ad.prototype = {
	width:null,
	height:null,
	src:null,
	url:null,
	text:null,
	adContainer: null,
        
	init:function(width, height, url, src, text){
		this.width = width;
		this.height = height;
		this.src = src;
		this.url = url;
		this.text = text;
	},

	getDomNode: function(){
		/* Create ad container */
		this.adContainer=document.createElement('div');
		this.adContainer.style.width= this.width.toString() + 'px';
		this.adContainer.style.height= this.height.toString() + 'px';
		// Do we deal with text or image ad?
		if (this.src == "") {
			this.adContainer.innerHTML = '<a target="_blank" style="text-decoration: none; " title="' + this.text + '" href="' + this.url + '">' + this.text + '</a>';
			this.adContainer.style.backgroundColor='#FFFFFF';
			this.adContainer.style.border='1px solid black';
			this.adContainer.style.padding='3px';
			this.adContainer.style.font='12px arial,helvetica,clean,sans-serif';
		}
		else {
			this.adContainer.innerHTML = '<a target="_blank" title="' + this.text + '" href="' + this.url + '"><img alt="' + this.text + '" src="' + this.src + '" style="border:0px;"></a>';
		}
		return this.adContainer;
	}
}

/* A method for testing if a point is inside a polygon
* Returns true if polygon contains point
* Algorithm described at http://alienryderflex.com/polygon/ 
*/
GPolygon.prototype.Contains = function(point) {
	var j=0;
	var oddNodes = false;
	var x = point.lng();
	var y = point.lat();
	var vertexCount = this.getVertexCount();
	for (var i=0; i < vertexCount; i++) {
		j++;
		if (j == vertexCount) {
			j = 0;
		}
		if (((this.getVertex(i).lat() < y) && (this.getVertex(j).lat() >= y)) 
			|| ((this.getVertex(j).lat() < y) && (this.getVertex(i).lat() >= y))) {
			if ( this.getVertex(i).lng() + (y - this.getVertex(i).lat())
				/  (this.getVertex(j).lat()-this.getVertex(i).lat())
				*  (this.getVertex(j).lng() - this.getVertex(i).lng())<x ) {
				oddNodes = !oddNodes
			}
		}
	}
	return oddNodes;
}

/* 
* A method for testing if a point is inside a polyline
*/
GPolyline.prototype.Contains = GPolygon.prototype.Contains;


/*
 * Ext JS Library 2.0.1
 * Copyright(c) 2006-2007, Ext JS, LLC.
 * licensing@extjs.com
 * 
 * http://extjs.com/license
 */

/**
 * @class Ext.ProgressBar
 * @extends Ext.BoxComponent
 * <p>An updateable progress bar component.  The progress bar supports two different modes: manual and automatic.</p>
 * <p>In manual mode, you are responsible for showing, updating (via {@link #updateProgress}) and clearing the
 * progress bar as needed from your own code.  This method is most appropriate when you want to show progress
 * throughout an operation that has predictable points of interest at which you can update the control.</p>
 * <p>In automatic mode, you simply call {@link #wait} and let the progress bar run indefinitely, only clearing it
 * once the operation is complete.  You can optionally have the progress bar wait for a specific amount of time
 * and then clear itself.  Automatic mode is most appropriate for timed operations or asymchronous operations in
 * which you have no need for indicating intermediate progress.</p>
 * @cfg {Float} value A floating point value between 0 and 1 (e.g., .5, defaults to 0)
 * @cfg {String} text The progress bar text (defaults to '')
 * @cfg {Mixed} textEl The element to render the progress text to (defaults to the progress
 * bar's internal text element)
 * @cfg {String} id The progress bar element's id (defaults to an auto-generated id)
 */
Ext.ProgressBar = Ext.extend(Ext.BoxComponent, {
   /**
    * @cfg {String} baseCls
    * The base CSS class to apply to the progress bar's wrapper element (defaults to 'x-progress')
    */
    baseCls : 'x-progress',

    // private
    waitTimer : null,

    // private
    initComponent : function(){
        Ext.ProgressBar.superclass.initComponent.call(this);
        this.addEvents(
            /**
             * @event update
             * Fires after each update interval
             * @param {Ext.ProgressBar} this
             * @param {Number} The current progress value
             * @param {String} The current progress text
             */
            "update"
        );
    },

    // private
    onRender : function(ct, position){
        Ext.ProgressBar.superclass.onRender.call(this, ct, position);

        var tpl = new Ext.Template(
            '<div class="{cls}-wrap">',
                '<div class="{cls}-inner">',
                    '<div class="{cls}-bar">',
                        '<div class="{cls}-text">',
                            '<div>&#160;</div>',
                        '</div>',
                    '</div>',
                    '<div class="{cls}-text {cls}-text-back">',
                        '<div>&#160;</div>',
                    '</div>',
                '</div>',
            '</div>'
        );

        if(position){
            this.el = tpl.insertBefore(position, {cls: this.baseCls}, true);
        }else{
            this.el = tpl.append(ct, {cls: this.baseCls}, true);
        }
        if(this.id){
            this.el.dom.id = this.id;
        }
        var inner = this.el.dom.firstChild;
        this.progressBar = Ext.get(inner.firstChild);

        if(this.textEl){
            //use an external text el
            this.textEl = Ext.get(this.textEl);
            delete this.textTopEl;
        }else{
            //setup our internal layered text els
            this.textTopEl = Ext.get(this.progressBar.dom.firstChild);
            var textBackEl = Ext.get(inner.childNodes[1]);
            this.textTopEl.setStyle("z-index", 99).addClass('x-hidden');
            this.textEl = new Ext.CompositeElement([this.textTopEl.dom.firstChild, textBackEl.dom.firstChild]);
            this.textEl.setWidth(inner.offsetWidth);
        }
        if(this.value){
            this.updateProgress(this.value, this.text);
        }else{
            this.updateText(this.text);
        }
        this.setSize(this.width || 'auto', 'auto');
        this.progressBar.setHeight(inner.offsetHeight);
    },

    /**
     * Updates the progress bar value, and optionally its text.  If the text argument is not specified,
     * any existing text value will be unchanged.  To blank out existing text, pass ''.  Note that even
     * if the progress bar value exceeds 1, it will never automatically reset -- you are responsible for
     * determining when the progress is complete and calling {@link #reset} to clear and/or hide the control.
     * @param {Float} value (optional) A floating point value between 0 and 1 (e.g., .5, defaults to 0)
     * @param {String} text (optional) The string to display in the progress text element (defaults to '')
     * @return {Ext.ProgressBar} this
     */
    updateProgress : function(value, text){
        this.value = value || 0;
        if(text){
            this.updateText(text);
        }
        var w = Math.floor(value*this.el.dom.firstChild.offsetWidth);
        this.progressBar.setWidth(w);
        if(this.textTopEl){
            //textTopEl should be the same width as the bar so overflow will clip as the bar moves
            this.textTopEl.removeClass('x-hidden').setWidth(w);
        }
        this.fireEvent('update', this, value, text);
        return this;
    },

    /**
     * Initiates an auto-updating progress bar.  A duration can be specified, in which case the progress
     * bar will automatically reset after a fixed amount of time and optionally call a callback function
     * if specified.  If no duration is passed in, then the progress bar will run indefinitely and must
     * be manually cleared by calling {@link #reset}.  The wait method accepts a config object with
     * the following properties:
     * <pre>
Property   Type          Description
---------- ------------  ----------------------------------------------------------------------
duration   Number        The length of time in milliseconds that the progress bar should
                         run before resetting itself (defaults to undefined, in which case it
                         will run indefinitely until reset is called)
interval   Number        The length of time in milliseconds between each progress update
                         (defaults to 1000 ms)
increment  Number        The number of progress update segments to display within the progress
                         bar (defaults to 10).  If the bar reaches the end and is still
                         updating, it will automatically wrap back to the beginning.
fn         Function      A callback function to execute after the progress bar finishes auto-
                         updating.  The function will be called with no arguments.  This function
                         will be ignored if duration is not specified since in that case the
                         progress bar can only be stopped programmatically, so any required function
                         should be called by the same code after it resets the progress bar.
scope      Object        The scope that is passed to the callback function (only applies when
                         duration and fn are both passed).
</pre>
         *
         * Example usage:
         * <pre><code>
var p = new Ext.ProgressBar({
   renderTo: 'my-el'
});

//Wait for 5 seconds, then update the status el (progress bar will auto-reset)
p.wait({
   interval: 100, //bar will move fast!
   duration: 5000,
   increment: 15,
   scope: this,
   fn: function(){
      Ext.fly('status').update('Done!');
   }
});

//Or update indefinitely until some async action completes, then reset manually
p.wait();
myAction.on('complete', function(){
    p.reset();
    Ext.fly('status').update('Done!');
});
</code></pre>
     * @param {Object} config (optional) Configuration options
     * @return {Ext.ProgressBar} this
     */
    wait : function(o){
        if(!this.waitTimer){
            var scope = this;
            o = o || {};
            this.waitTimer = Ext.TaskMgr.start({
                run: function(i){
                    var inc = o.increment || 10;
                    this.updateProgress(((((i+inc)%inc)+1)*(100/inc))*.01);
                },
                interval: o.interval || 1000,
                duration: o.duration,
                onStop: function(){
                    if(o.fn){
                        o.fn.apply(o.scope || this);
                    }
                    this.reset();
                },
                scope: scope
            });
        }
        return this;
    },

    /**
     * Returns true if the progress bar is currently in a {@link #wait} operation
     * @return {Boolean} True if waiting, else false
     */
    isWaiting : function(){
        return this.waitTimer != null;
    },

    /**
     * Updates the progress bar text.  If specified, textEl will be updated, otherwise the progress
     * bar itself will display the updated text.
     * @param {String} text (optional) The string to display in the progress text element (defaults to '')
     * @return {Ext.ProgressBar} this
     */
    updateText : function(text){
        this.text = text || '&#160;';
        this.textEl.update(this.text);
        return this;
    },

    /**
     * Sets the size of the progress bar.
     * @param {Number} width The new width in pixels
     * @param {Number} height The new height in pixels
     * @return {Ext.ProgressBar} this
     */
    setSize : function(w, h){
        Ext.ProgressBar.superclass.setSize.call(this, w, h);
        if(this.textTopEl){
            var inner = this.el.dom.firstChild;
            this.textEl.setSize(inner.offsetWidth, inner.offsetHeight);
        }
        return this;
    },

    /**
     * Resets the progress bar value to 0 and text to empty string.  If hide = true, the progress
     * bar will also be hidden (using the {@link #hideMode} property internally).
     * @param {Boolean} hide (optional) True to hide the progress bar (defaults to false)
     * @return {Ext.ProgressBar} this
     */
    reset : function(hide){
        this.updateProgress(0);
        if(this.textTopEl){
            this.textTopEl.addClass('x-hidden');
        }
        if(this.waitTimer){
            this.waitTimer.onStop = null; //prevent recursion
            Ext.TaskMgr.stop(this.waitTimer);
            this.waitTimer = null;
        }
        if(hide === true){
            this.hide();
        }
        return this;
    }
});
Ext.reg('progress', Ext.ProgressBar);