function Map() {
	var THIS = this; //  when "this" is redefined, use "THIS" to access the properties
	//
	this.contexts = '';
	this.map = '';
	this.markers = '';
	this.intersectPadding = 0;
	//
	this.onMoveStart = function() {};
	this.onMoveEnd = function() {};
	this.onZoomEnd = function() {};
	this.onContextClick = function() {};
	//    
	this.refresh = function() 
	{
		var zoom = this.map.getZoom();
		var projection = this.map.getCurrentMapType().getProjection();

		// filter non-visible markers
		// create one marker cluster for each visible marker
		var activeMarkers = [];
		var i;
		for (i in this.markers) 
		{
			var marker = this.markers[i];
			if (zoom >= marker.context.zoomMin && zoom <= marker.context.zoomMax ) 
			{
				activeMarkers.push(marker);
				marker.cluster = [];
				marker.cluster.push(marker);
			}
		}

		// reset markers to their original positions
		for (i in activeMarkers) {
			marker = activeMarkers[i];
			marker.setLatLng(new GLatLng(marker.context.lat, marker.context.lon));
		}

		// calculate marker icon bounds
		for (i in activeMarkers) {
			marker = activeMarkers[i];
			var latLng = marker.getLatLng();
			var iconSize = marker.getIcon().iconSize;
			var iconAnchorPoint = projection.fromLatLngToPixel(latLng, zoom);
			var iconAnchorPointOffset = marker.getIcon().iconAnchor;
			var iconBoundsPointSw = new GPoint(iconAnchorPoint.x-iconAnchorPointOffset.x-this.intersectPadding, iconAnchorPoint.y-iconAnchorPointOffset.y+iconSize.height+this.intersectPadding);
			var iconBoundsPointNe = new GPoint(iconAnchorPoint.x-iconAnchorPointOffset.x+iconSize.width+this.intersectPadding, iconAnchorPoint.y-iconAnchorPointOffset.y-this.intersectPadding);
			var iconBoundsLatLngSw = projection.fromPixelToLatLng(iconBoundsPointSw, zoom);
			var iconBoundsLatLngNe = projection.fromPixelToLatLng(iconBoundsPointNe, zoom);
			marker.iconBounds = new GLatLngBounds(iconBoundsLatLngSw, iconBoundsLatLngNe);
		}
		
		// group overlapping markers into same clusters
		var clusters = [];
		for (i=0 ; i<activeMarkers.length ; i++) {
			var marker1 = activeMarkers[i];
			var newCluster = true;
			for (var j=i+1 ; j<activeMarkers.length ; j++) {
				var marker2 = activeMarkers[j];
				if (marker1.iconBounds.intersects(marker2.iconBounds)) {
					if (marker1.cluster.length >= marker2.cluster.length) {
//						marker2.cluster = marker1.cluster;
//						marker1.cluster.push(marker2);
					} else {
//						marker1.cluster = marker2.cluster;
//						marker2.cluster.push(marker1);
//						newCluster = false;
					}
				}
			}
			if (newCluster) 
			{
				clusters.push(marker1.cluster);
			}
		}
		
		// render clusters
		this.map.clearOverlays();
		for (var i  in clusters) {
			var markers = clusters[i];
			for (var j in markers) {
				marker = markers[j];
				this.map.addOverlay(marker);
			}
		}
	};
	//
	this.pan = function(lat, lon) 
	{
		var point = new GLatLng(lat, lon);
		this.map.panTo(point);
	};
	//
	this.centerByContexts = function() 
	{
		var b = new GLatLngBounds();
		for (var i in this.markers) {
			var marker = this.markers[i];
			b.extend(marker.getLatLng());
		}
		var zoom = this.map.getBoundsZoomLevel(b);
		if (zoom > 15) 
		{
			zoom = 15;
		}
		this.map.setCenter(b.getCenter(), zoom);
	};
	//
	this.centerByIP = function() 
	{		
		var zoom;
		if (google.loader.ClientLocation !== null) {
			this.map.setCenter(new GLatLng(google.loader.ClientLocation.latitude, google.loader.ClientLocation.longitude));
			zoom = 11;
		} else {
			this.map.setCenter(new GLatLng(IP_lat, IP_lon));
			zoom = 8;
		}
		this.map.setZoom(zoom);
	};
	this.setZoom = function(zoom) {
		this.map.setZoom(zoom);
	};
	//
	this.addContexts = function(contexts) 
	{
		for (var i in contexts) {
			var context = contexts[i];
			//
			// TEMP fix
			if (context.lat!=0 && context.lon!=0 && context.contextTypeId!=28 && context.contextTypeId!=29) {
				//
				var icon = new GIcon(G_DEFAULT_ICON);
				icon.shadow = "static/ui_images/icons/pin_shadow.png";
				icon.iconSize = new GSize(21, 31);
				icon.shadowSize = new GSize(38, 33);
				icon.iconAnchor = new GPoint(11, 30);
				icon.infoWindowAnchor = new GPoint(0, 0);
				icon.imageMap = [0,0, 21,0, 21,21, 0,21];
				
				var conType = context.contextTypeId;
				if (context.searchResult) {
					icon.image = "static/ui_images/icons/pin_" + site.getContextIconType(conType) + "_gray.png";
				} else {
					var alertLevel = site.getAlertLevelText(context.alertLevel);
					icon.image = "static/ui_images/icons/pin_" + site.getContextIconType(conType) + "_" + alertLevel + ".png";
				}
				
				var point = new GLatLng(context.lat, context.lon);
				var marker = new GMarker(point, {
					icon: icon			
				});
				marker.context = context;
				marker.map = this.map;
				
				GEvent.addListener(marker, "click", function(marker) {
					THIS.onContextClick(this.context);
				});
	 
				GEvent.addListener(marker, "mouseover", function() {
					tooltip.show("context", this.context);
				});
				GEvent.addListener(marker, "mouseout", function() {
					tooltip.hide();
				});
				//
				this.contexts.push(context);
				this.markers.push(marker);
			}
		}
		this.refresh();
	};
	//
	this.addContext = function(context) 
	{
		this.addContexts([context]);
	};
	//
	this.removeContexts = function(context) 
	{
		this.markers = [];
		this.contexts = [];
		this.refresh();
	};
	//
	this.moveStart = function() 
	{
		this.onMoveStart();
	};
	this.moveEnd = function() 
	{
		this.onMoveEnd();
		this.refresh();
	};
	this.zoomEnd = function() 
	{
		this.onZoomEnd();
		this.refresh();
	};
	
	//
	this.construct = function(map, type) 
	{
		this.map = map;

		this.markers = [];
		this.contexts = [];
		
		if (type=="context-manager") {
			this.map.addControl(new GSmallZoomControl3D(), new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(10,25)));
		} else {
			// no controls on "static" maps
		}

		GEvent.addListener(this.map, "movestart", delegate(this, this.moveStart));
		GEvent.addListener(this.map, "moveend", delegate(this, this.moveEnd));
		GEvent.addListener(this.map, "zoomend", delegate(this, this.zoomEnd));

	};
}

google.load("maps", "2.x");

console.log("map.js");