function CMap(containerID, latitude, longitude, zoom) {
    var container = document.getElementById(containerID);
    CMap.baseConstructor.call(this,container);

    latitude = latitude||39.774769485295465;
    longitude = longitude||-94.74609375;
    this.specifiedCenter = new GLatLng(latitude, longitude);
    this.zoom = zoom||4;
    
    this.setCenter(this.specifiedCenter, this.zoom);
    
    this.addMapType(G_PHYSICAL_MAP);
    this.addMapType(G_SATELLITE_3D_MAP);
    this.addControl(new GLargeMapControl3D());
    this.addControl(new GHierarchicalMapTypeControl());
    
    if (window["CPolygonControl"]) {
    	this.polygonControl = new CPolygonControl();
    	this.addControl(this.polygonControl);
    }
          
    this.markerManager = null;
    this.visibleOverlaysByName = {};
    this.shapesByName = new Object();
    var thisMap = this;
	this.label = null;
    this.refresh = function() {
        this.checkResize();
    }
    
    this.reset = function() {
        this.refresh();
        if (this.specifiedCenter) {
            try {
                this.setCenter(this.specifiedCenter, this.zoom);
            } catch (e) {}
        }
    }

    this.getMarkerManager = function() {
        if (! this.markerManager) {
            this.markerManager = new GMarkerManager(this);
        }
        return this.markerManager;
    }

    this.addLabel = function(label) {
    	this.removeLabel();
    	this.label = label;
    	this.addOverlay(label)
    }
    this.removeLabel = function() {
    	if (this.label) {
	    	this.removeOverlay(this.label)
	   	}
    }
    this.addOverlaysForMappables = function(mappables, doNotZoom) {
        if (!isArray(mappables)) {
            mappables = [mappables];
        }

        // we need to make sure we have a center before we apply any overlays
        if (this.getCenter() == null && this.specifiedCenter) {
            try {
                this.setCenter(this.specifiedCenter, this.zoom);
            } catch (e) {}
        }
        
        for (var i=0;i<mappables.length;i++) {
            var name = mappables[i].getName();
            this.removeOverlayByName(name);
            
            var marker = mappables[i].getMarker();
            if (marker) {
                this.visibleOverlaysByName[name] = marker;
                this.addOverlay(marker);
            }
        }

        if (!doNotZoom) {
            this.zoomToFitMappables();
        }
    }
    this.getOverlayByName = function(name) {
        return this.visibleOverlaysByName[name];
    }


    this.removeOverlayByName = function(name) {
        var marker = this.getOverlayByName(name);
        if (marker) {
            this.removeOverlay(marker);
        }
//        for (var key in this.visibleOverlaysByName) {
//            var value = this.visibleOverlaysByName[key];
//        }
        delete this.visibleOverlaysByName[name];
    }
    
    this.swapMappable = function(mappable) {
        this.removeOverlayByName(mappable.ID);
        var marker = mappable.getMarker();
        if (marker) {
            this.visibleOverlaysByName[name] = marker;
            this.addOverlay(marker);
        }
    }
    
    this.clearMappables = function() {
        for (var name in this.visibleOverlaysByName) {
            this.removeOverlayByName(name);
        }
        this.closeInfoWindow();
        this.clearOverlays();
        for (var name in this.shapesByName) {
            this.addOverlay(this.shapesByName[name]);
        }
        if (this.getUserShape()) {
            this.addOverlay(this.getUserShape());
        }
    }
    
    this.toggleInfoWindowByName = function(name) {
        var marker = this.getOverlayByName(name);
        if (marker) {
            if (this["openInfoWindowMarker"] && this.openInfoWindowMarker == marker) {
                this.closeInfoWindow();
                this.openInfoWindowMarker = null;
            } else {
                if (this["openInfoWindowMarker"] && this.openInfoWindowMarker != marker) {
                    this.closeInfoWindow();
                }
                marker.openInfoWindow();
            }
        }
    }
    this.drawPolygon = function(name, polygon, isFilled) {
        this.removeShapeByName(name);
        var color = "#000099";
        color = (name=="user"?"#FF0000":color);
        var startImage = "/images/map/dot_blue.png";
        startImage = (name=="user"?"/images/map/dot_red.png":startImage);
        if (polygon.points.length) {
            if (polygon.points.length == 1) {
                var icon = new GIcon();
                icon.image = startImage;
                icon.iconSize = new GSize(4, 4);
                icon.shadow = "";
                icon.iconAnchor = new GPoint(2, 2);
                this.shapesByName[name] = new GMarker(polygon.points[0],icon);
            } else if (isFilled) {
                this.shapesByName[name] =  new GPolygon(polygon.points,color, 4, 0.5,  color,  0.1);
            } else {
                this.shapesByName[name] =  new GPolyline(polygon.points,color, 4, 0.5);
            }
            if (!this.getCenter()) {
                this.reset()
            }
            this.addOverlay(this.shapesByName[name]);
        }
    }
    
    this.getUserShape = function() {
       var shape = null;
       if (this.polygonControl && this.polygonControl.polygon) {
           shape = this.polygonControl.polygon;
       } else if (this.shapesByName["user"]) {
           shape = this.shapesByName["user"];
       }
       return shape;
    }
    this.hasUserShape = function() {
    	return this.getUserShape();
    }
    this.clearUserShape = function() {
        this.removeShapeByName("user");
        if (this.polygonControl && this.polygonControl.polygon) {
            this.removeOverlay(this.polygonControl.polygon)
        }
    }
    
    
    this.removeShapeByName = function(name) {
        if (this.shapesByName[name]) {
            this.removeOverlay(this.shapesByName[name]);
            GEvent.clearInstanceListeners(this.shapesByName[name]);
            delete this.shapesByName[name];
        }
    }
    
    this.zoomToFitMappables = function() {
        var north = 0
        var south = 0
        var east = 0
        var west = 0;
        var lats = new Array();
        var lngs = new Array();
        var points = new Array();
        var distances = new Array();
        //if we have a shape, then zoom to fit the shape, otherwise zoom in on the points
        var shape = this.getUserShape();
        shape = (shape?shape:this.shapesByName["error"]);
        if (shape) {
            for (var i=0;i<shape.getVertexCount();i++) {
                var pt = shape.getVertex(i);
                if (pt.lat() != -9090909 && (!north || pt.lat() > north)) {
                    north = parseFloat(pt.lat());
                }
                if (pt.lat() != -9090909 && (!south || pt.lat() < south)) {
                    south = parseFloat(pt.lat());
                }
                if (pt.lng() != -9090909 && (!east || pt.lng() > east)) {
                    east = parseFloat(pt.lng());
                }
                if (pt.lng() != -9090909 && (!west || pt.lng() < west)) {
                    west = parseFloat(pt.lng());
                } 
                if (pt.lat() != -9090909 && pt.lng() != -9090909) {
                    points.push(pt);
                    lats.push(pt.lat());
                    lngs.push(pt.lng());
                }
            }
        } else { 
            for (var name in this.visibleOverlaysByName) {
                var marker = this.visibleOverlaysByName[name];
                var pt = marker.getLatLng();
                if (pt.lat() != -9090909 && (!north || pt.lat() > north)) {
                    north = parseFloat(pt.lat());
                }
                if (pt.lat() != -9090909 && (!south || pt.lat() < south)) {
                    south = parseFloat(pt.lat());
                }
                if (pt.lng() != -9090909 && (!east || pt.lng() > east)) {
                    east = parseFloat(pt.lng());
                }
                if (pt.lng() != -9090909 && (!west || pt.lng() < west)) {
                    west = parseFloat(pt.lng());
                }
                if (pt.lat() != -9090909 && pt.lng() != -9090909) {
                    points.push(pt);
                    lats.push(pt.lat());
                    lngs.push(pt.lng());
                }
            }
        }
        
        //find the "mean point" to act as centroid
        var sortFunc = function(a,b) {
            a = eval(a);
            b = eval(b);
            var s = 0;
            if (a < b) {
                s = -1;
            } else if (a > b) {
                s = 1;
            }
            return s;
        }
        lats.sort(sortFunc);
        lngs.sort(sortFunc);
        var origin = new GLatLng(lats[Math.round(lats.length/2)], lngs[Math.round(lngs.length/2)]);
        
        //mesaure the distances for each point from the southeast
        for (var i=0;i < points.length;i++) {
            distances.push(origin.distanceFrom(points[i]));
        }
        
        //find the stdviation
        var summedDistances = 0;
        mapFunction(function(x){summedDistances+=x;},distances);
        var averageDistance = summedDistances/distances.length;
        var sumOfSquaresOfDistances = 0;
        mapFunction(function(x){sumOfSquaresOfDistances+=Math.pow(x,2);},distances);
        var variance = (sumOfSquaresOfDistances - (Math.pow(summedDistances,2)/distances.length))/distances.length;
        var standardDeviation = Math.sqrt(variance);
        
        //iterate over all of the points and make a new list of the ones that are inside 2 stddeviations
        var newPoints = filterFunction(function(x){return (origin.distanceFrom(x) < 2*standardDeviation)},points);
        if (points.length - newPoints.length <= 1) {
            points = newPoints;
        }

        //nowfind the bounds
        north = 0
        south = 0
        east = 0
        west = 0;
        for (var i=0;i < points.length;i++) {
            var pt = points[i];
            if ((!north || pt.lat() > north)) {
                north = parseFloat(pt.lat());
            }
            if ((!south || pt.lat() < south)) {
                south = parseFloat(pt.lat());
            }
            if ((!east || pt.lng() > east)) {
                east = parseFloat(pt.lng());
            }
            if ((!west || pt.lng() < west)) {
                west = parseFloat(pt.lng());
            }
        }        
        if (north || south || east || west) {
            this.zoomToFit(north,south,east,west);
        }
    }

    this.zoomToFit = function(north,south,east,west) {
        north  = parseFloat(north);
        south  = parseFloat(south);
        east  = parseFloat(east);
        west  = parseFloat(west);

        var bounds = new GLatLngBounds(new GLatLng(south,west), new GLatLng(north,east));
        var zoom = this.getBoundsZoomLevel(bounds);


        this.zoom = (zoom < 1)?1:zoom;

        var midLongitude = ((east - west)/2 + west);
        var midLatitude = ((north - south)/2 + south);
        this.specifiedCenter = bounds.getCenter();
        //don't zoom if we have -9090909 in there, or if the center is exactly 0/0
        if ((this.specifiedCenter.lng() > -181 && this.specifiedCenter.lat() > -181) && (this.specifiedCenter.lng() != 0 && this.specifiedCenter.lat() != 0)) {
            this.reset();
        }
    }
    
    this.zoomTo = function(mappable) {
        var point = (mappable.point)?mappable.point:mappable;
        this.zoomToFit(point.lat(),point.lat(),point.lng(),point.lng());
    }

    this.isInHighResBounds = function() {
        var poly = new Polygon();
        poly.addPoint(new GPoint(-139.0430,60.0429));
        poly.addPoint(new GPoint(-64.7843,60.8329));
        poly.addPoint(new GPoint(-65.2567,18.0721));
        poly.addPoint(new GPoint(-68.0383,17.8088));
        poly.addPoint(new GPoint(-80.4199,24.4071));
        poly.addPoint(new GPoint(-97.4268,25.6811));
        poly.addPoint(new GPoint(-124.1012,27.6835));
        poly.close();
        return poly.containsPoint(this.getCenterLatLng());

    }

    //prevent zooming higher than leven 8 outside of north america
    GEvent.addListener(this,"zoom", function(oldZoom,newZoom) {
        if (newZoom < 8 && !map.isInHighResBounds()) {
            map.zoomTo(8);
        }
    });
}

//this goofy guy here allows us to more precisely subclass
function extend(subClass, baseClass) {
  function inheritance() {}
  inheritance.prototype = baseClass.prototype;

  subClass.prototype = new inheritance();
  subClass.prototype.constructor = subClass;
  subClass.baseConstructor = baseClass;
  subClass.superClass = baseClass.prototype;
}

extend(CMap, GMap2);


