////////////////////////////////////////////////////////////////////////////////////////////
// MappingControl mapping client-side script
////////////////////////////////////////////////////////////////////////////////////////////

addNamespace("Mapping");

Mapping.Maps = new Core.Collection();

////////////////////////////////////////////////////////////////////////////////////////////

Mapping.SymbolTypeClass = Class.inherit(Core.Enum, 
{
	Circle: 1,
	Square: 2,
	Diamond: 3,
	Star: 4,
	Triangle: 5,
	Cross: 6,
	Pin: 7,
	Bitmap: 50,
	Text: 80
});
Mapping.SymbolType = new Mapping.SymbolTypeClass("Mapping.SymbolTypeClass");

Mapping.ZoomOptionsClass = Class.inherit(Core.Enum, 
{
	ZoomIn: 1,
	ZoomOut: 2
});
Mapping.ZoomOptions = new Mapping.ZoomOptionsClass("Mapping.ZoomOptionsClass");

Mapping.SelectionTypeClass = Class.inherit(Core.Enum, 
{
	Replace: 0,
	AddTo: 1,
	RemoveFrom: 2
});
Mapping.SelectionType = new Mapping.SelectionTypeClass("Mapping.SelectionTypeClass");

Mapping.DistanceUnitClass = Class.inherit(Core.Enum, 
{
	Centimeter: 0,
	Chain: 1,
	Degree: 2,
	Foot: 3,
	Inch: 4,
	Kilometer: 5,
	Link: 6,
	Meter: 7,
	Mile: 8,
	Millimeter: 9,
	NauticalMile: 10,
	Rod: 11,
	SurveyFoot: 12,
	Yard: 13
});
Mapping.DistanceUnit = new Mapping.DistanceUnitClass("Mapping.DistanceUnitClass");

Mapping.LayerTypeClass = Class.inherit(Core.Enum, 
{
	Drilldown: 0,
	Grid: 1,
	Group: 2,
	Label: 3,
	Normal: 4,
	ObjectTheme: 5,
	Raster: 6,
	Seamless: 7,
	Unknown: 8,
	UserDrawn: 9,
	Wms: 10
});
Mapping.LayerType = new Mapping.LayerTypeClass("Mapping.LayerTypeClass");

Mapping.TableSourceClass = Class.inherit(Core.Enum, 
{
	Csv: 0,
	DBase: 1,
	SqlServer: 2,
	Tab: 3
});
Mapping.TableSource = new Mapping.TableSourceClass("Mapping.TableSourceClass");

Mapping.VisibleRangeTypeClass = Class.inherit(Core.Enum, 
{
	Zoom: 0,
	Scale: 1
});
Mapping.VisibleRangeType = new Mapping.VisibleRangeTypeClass("Mapping.VisibleRangeTypeClass");

////////////////////////////////////////////////////////////////////////////////////////////

Mapping.LayerToolInformation = Class.create();
Mapping.LayerToolInformation.prototype =
{
	initialize: function(selectable, displayInformation)
	{
		this.selectable = selectable == true;
		this.displayInformation = displayInformation == true;
	}
};

////////////////////////////////////////////////////////////////////////////////////////////

Mapping.SettingsClass = Class.create();
Mapping.SettingsClass.prototype =
{
	initialize: function()
	{
		this.reset();
	},
	
	reset: function()
	{
		this.font = new Drawing.Font();
		this.color = Drawing.Colors.Black;
		this.backgroundColor = Drawing.Colors.Transparent;
		this.haloColor = Drawing.Colors.Transparent;
		this.size = 14;
		this.bitmapName = "";
		this.symbol = Mapping.SymbolType.Star;
	}
};

////////////////////////////////////////////////////////////////////////////////////////////

Mapping.Map = Base.extend(
{
    constructor: function(id, key, useTiles) {
        if (!key)
            var key = id;

        this.id = id;
        this.key = key;

        if (Mapping.Maps.contains(this.id))
            Mapping.Maps.remove(this.id);

        Mapping.Maps.add(this);

        this.image = document.getElementById(this.id + "Image");
        this.container = document.getElementById(this.id);
        this.cachedImage = new Image();
        this.onloading = null;
        this.onloaded = null;
        this.onmessage = null;
        this.oninformation = null;
        this.preCacheSize = 0;

        this.useTiles = !!useTiles;
        this.tiles = [];
        this.tileSize = 256;
        this.zoomLevel = 2;
        this._resetTiles();

        this._updatePosition();
        this.showLoading = true;

        this._message = new Drawing.Text();
        this._message.parent = this.container;
        this._message.redraw = false;
        this._message.nowrap = true;
        this._message.font.size = 11;
        this._message.font.bold = true;
        this._message.backColor = Drawing.Colors.White;

        this._information = new Drawing.Text();
        this._information.parent = this.container;
        this._information.redraw = false;
        this._information.nowrap = true;
        this._information.font.size = 11;
        this._information.backColor = Drawing.Colors.White;

        this._messageKey = null;
        this._informationKey = null;

        this.drawingSettings = new Mapping.SettingsClass();
        this.toolSettings = new Tools.SettingsClass();
        this.serverEvents = new Core.Collection();

        this.error = null;
        this.onerror = null;

        window.onresize = this._updatePosition.bindAsEventListener(this);

        this.setSize();

        this._clear();
        this.baseImageUrl = this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetImageUrl(this.key));
        this.baseTileUrl = this.baseImageUrl.replace("/map/", "/tile/");

        if (this.useTiles) {
            this.setZoomLevel(15);
            this.updateTiles();
        }
    },

    _resetTiles: function() {
        // remove any current tiles
        for (var i = 0; i < this.tiles.length; i++) {
            var tileArray = this.tiles[i];

            for (var t = 0; t < tileArray.length; t++) {
                var tile = tileArray[t];

                if (tile != null)
                    this.image.removeChild(tile);
            }
        }

        // initialise new tile arrays
        for (var i = 1; i < Mapping.Map.zoomScale.length; i++)
            this.tiles[i] = [];
    },

    _get: function(result) {
        this.error = result.error;

        if (this.error != null && this.onerror != null)
            this.onerror(this.error);

        return result.value;
    },

    _clear: function() {
        this.error = null;
    },

    _updatePosition: function() {
        this.top = Core.Utility.getElementY(this.container);
        this.left = Core.Utility.getElementX(this.container);
        this.width = Core.Utility.getElementWidth(this.container);
        this.height = Core.Utility.getElementHeight(this.container);
    },

    // get the map tiles for the current view
    updateTiles: function() {
        var center = this.getCenter();
        Core.Debug.write("updateTiles for " + center);
        if (!center) return;

        var qtree = this.toQuadtree(center.X, center.Y, this.zoomLevel);
        var tileArray = this.tiles[this.zoomLevel];

        Core.Debug.write("updateTiles (qtree: " + qtree + ", level: " + this.zoomLevel + ")");

        // adjust tile to give top left
        var centerOffsetX = Math.floor(this.width / 2);
        var cols = Math.ceil(this.width / this.tileSize) + 1;
        var adjustmentLeft = Math.ceil(centerOffsetX / this.tileSize);
        var centerOffsetY = Math.floor(this.height / 2);
        var rows = Math.ceil(this.height / this.tileSize) + 1;
        var adjustmentUp = Math.ceil(centerOffsetY / this.tileSize);
        qtree = this.moveQuadrant(qtree, -adjustmentLeft, -adjustmentUp);

        var scaleFactor = Mapping.Map.zoomScale[1] / Mapping.Map.zoomScale[this.zoomLevel];
        this.image.style.width = Core.Utility.toSize(scaleFactor * this.tileSize);
        this.image.style.height = Core.Utility.toSize(scaleFactor * this.tileSize);

        Core.Debug.write("Container size = " + (scaleFactor * this.tileSize));

        for (var row = 0; row <= rows; row++) {
            for (var col = 0; col <= cols; col++) {
                var quad = this.moveQuadrant(qtree, col, row);
                var xyz = this.toXYZoom(quad);

                // do we already have this image?
                var tileRow = /*Math.floor(*/(Mapping.Map.zoomScale[1] - Mapping.Map.zoomScale[this.zoomLevel] - xyz.y) / Mapping.Map.zoomScale[this.zoomLevel]/*)*/,
					tileCol = /*Math.floor(*/xyz.x / Mapping.Map.zoomScale[this.zoomLevel]/*)*/,
					img;

                if (tileRow < 0 || tileCol < 0)
                    continue;

                if (/*tileArray.length > tileRow && */tileArray["@" + tileRow] != null /*&& tileArray[tileRow].length > tileCol*/ && tileArray["@" + tileRow]["@" + tileCol] != null) {
                    Core.Debug.write("Loading tile from cache: " + tileRow + ", " + tileCol + " @ " + this.zoomLevel);
                    img = tileArray["@" + tileRow]["@" + tileCol];
                }
                else {
                    img = this.image.appendChild(document.createElement("img"));
                    img.style.width = Core.Utility.toSize(this.tileSize);
                    img.style.height = Core.Utility.toSize(this.tileSize);
                    img.style.left = Core.Utility.toSize(tileCol * this.tileSize);
                    img.style.top = Core.Utility.toSize(tileRow * this.tileSize);
                    img.style.position = "absolute";
                    img.src = this.getTileImageUrl(quad);
                    img.qtree = quad;

                    if (tileArray["@" + tileRow] == null)
                        tileArray["@" + tileRow] = [];

                    tileArray["@" + tileRow]["@" + tileCol] = img;

                    Core.Debug.write("Tile (" + xyz.x + ", " + xyz.y + ") @ r" + tileRow + ", c" + tileCol + ": l=" + img.style.left + ", t=" + img.style.top);
                }

                img.style.display == "";
            }

            Core.Debug.write("IMGS: " + (document.getElementsByTagName("img").length - 11));
        }

        // offset the tile container div
        this.image.style.left = Core.Utility.toSize(-Math.floor((center.X / Mapping.Map.zoomScale[this.zoomLevel]) * this.tileSize) + centerOffsetX);
        this.image.style.top = Core.Utility.toSize(-Math.floor(((Mapping.Map.zoomScale[1] - center.Y) / Mapping.Map.zoomScale[this.zoomLevel]) * this.tileSize) + centerOffsetY);

        Core.Debug.write("Image (" + this.image.style.left + ", " + this.image.style.top + ")");
    },

    // show the specified message on the map
    showMessage: function(text, key) {
        if (key)
            this._messageKey = key;
        else
            this._messageKey = null;

        if (this.onmessage) {
            if (this.onmessage(text))
                return;
        }

        this._message.text = "&nbsp;" + text + "&nbsp;";
        this._message.positionAt(0, 0);
        this._message.update(true);
        this._message._control.style.zIndex = 500;
        this._message._control.style.textAlign = "center";
    },

    // hide the message with the specified key from the map
    hideMessage: function(key) {
        if (!key || key == this._messageKey) {
            if (this.onmessage) {
                if (this.onmessage(""))
                    return;
            }

            this._message.hide();
        }
    },

    // show the specified information on the map
    showInformation: function(text, key) {
        if (key)
            this._informationKey = key;
        else
            this._informationKey = null;

        if (this.oninformation) {
            if (this.oninformation(text, key))
                return;
        }

        this._information.text = "&nbsp;" + text + "&nbsp;";
        //this._information.move(0, 0, this.width, this._information.font.size + 5);
        this._information.move(0, this.height - this._information.font.size - 5, this.width, this.height);
        this._information.update(true);
        this._information._control.style.zIndex = 500;
    },

    // hide the information with the specified key from the map
    hideInformation: function(key) {
        if (!key || key == this._informationKey) {
            if (this.oninformation) {
                if (this.oninformation("", key))
                    return;
            }

            this._information.hide();
        }
    },

    // update the coordinates
    move: function(left, top, right, bottom) {
        this._clear();

        if (left)
            this.container.style.left = this.left = Core.Utility.toSize(left);

        if (top)
            this.container.style.top = this.top = Core.Utility.toSize(top);

        if (right)
            this.container.style.right = this.right = Core.Utility.toSize(right);

        if (bottom)
            this.container.style.bottom = this.bottom = Core.Utility.toSize(bottom);

        this.width = Core.Utility.getElementWidth(this.container);
        this.height = Core.Utility.getElementHeight(this.container);
    },

    // refresh the map image
    refresh: function(cache) {
        this._updatePosition(); // BUGFIX
        this.move(); // BUGFIX
        if (this.showLoading) {
            this.showMessage("loading...", "map");

            if (Drawing.Window.getCursor(this.image) != "wait")
                this._cursor = Drawing.Window.getCursor(this.image);

            Drawing.Window.setCursor("wait", this.image);

            if (this.onloading)
                this.onloading();
        }

        if (this.useTiles) {
            // tile refresh
            this.updateTiles();
            this._resetPosition();
        }
        else {
            // single image refresh
            var url = this.getImageUrl();

            if (url.indexOf("?") > -1)
                url += "&" + Math.random();
            else
                url += "?" + Math.random();

            if (cache) {
                this.cachedImage.onload = this._loadFromCache.bindAsEventListener(this);
                this.cachedImage.style.display = "none";
                this.cachedImage.src = url;
            }
            else {
                this.cachedImage.style.display = "none";
                this.image.onload = this._resetPosition.bindAsEventListener(this);
                this.image.src = url;
            }
        }
    },

    getImageUrl: function(width, height) {
        if (width == null) width = this.width;
        if (height == null) height = this.height;

        //if (width == 0)
        //	alert("width = 0, this.width = " + this.width);

        return this.baseImageUrl + "?Size=" + width + "," + height;
    },

    getTileImageUrl: function(qtree) {
        return this.baseTileUrl + "?q=" + qtree + "&w=" + this.tileSize + "&h=" + this.tileSize;
    },

    // show or hide a scale bar on the map
    showScaleBar: function(visible, refresh) {
        if (!visible)
            visible = true;

        this._clear();
        var result = this._get(Torbay.GIS.Web.Controls.MappingControlActions.ShowScaleBar(this.key, visible));

        if (refresh)
            this.refresh(true);

        return result;
    },

    // determine whether a scale bar is visible on the map
    isScaleBarVisible: function() {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.IsScaleBarVisible(this.key));
    },

    // call remote invoke on the service
    execute: function(methodName, parameters, refresh) {
        this._clear();
        var result = this._get(Torbay.GIS.Web.Controls.MappingControlActions.ExecuteMappingMethod(this.key, methodName, parameters));

        if (refresh)
            this.refresh(true);

        return result;
    },

    // convert screen to map coordinates
    convertScreenToMapCoordinates: function(point) {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.ConvertScreenToMapCoordinates(this.key, point));
    },

    // convert map to screen coordinates
    convertMapToScreenCoordinates: function(point) {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.ConvertMapToScreenCoordinates(this.key, point));
    },

    // return an array of TableInformation objects representing the tables in the map
    getTables: function() {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetTables(this.key));
    },

    // return a TableInformation object representing the specified table in the map
    getTable: function(table) {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetTable(this.key, table));
    },

    // return an array of strings representing the table names in the map
    getTableNames: function() {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetTableNames(this.key));
    },

    // return an array of strings representing the column names in the specified table
    getTableColumnNames: function(table) {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetTableColumnNames(this.key, table));
    },
    // return an array of strings representing the column names and data type in the specified table
    getTableColumnNamesAndType: function(table) {
        this._clear();
        var result = this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetTableColumnNamesAndType(this.key, table));
        //return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetTableColumnNamesAndType(this.key, table));

        //Convert result Array into JSON object
        var jsonArray = new Array();
        var jsonRecord, colName, colType, separatorPos;
        for (var i = 0; i < result.length; i++) {
            separatorPos = result[i].indexOf("|");
            colName = result[i].substring(0, separatorPos);
            colType = result[i].substring(separatorPos + 1, result[i].length);
            jsonRecord = { ColumnName: colName, DataType: colType };
            jsonArray[i] = jsonRecord;
        }
        return jsonArray;

    },

    // get whether the specified table is displayed in the map
    getTableIsDisplayedInMap: function(table) {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetTableIsDisplayedInMap(this.key, table));
    },

    // return an array of strings representing the layer names in the map
    getLayerNames: function(onlyVisible) {
        this._clear();

        if (onlyVisible)
            return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetLayerNamesVisible(this.key));
        else
            return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetLayerNames(this.key));
    },

    // return an array layer in the map as a LayerInformation object
    getLayers: function(onlyVisible) {
        this._clear();

        if (onlyVisible)
            return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetLayersVisible(this.key));
        else
            return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetLayers(this.key));
    },

    // return a layer in the map as a LayerInformation object
    getLayer: function(name) {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetLayer(this.key, name));
    },

    // remove the named layer from the map
    removeLayer: function(name, refresh) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.RemoveLayer(this.key, name));

        if (refresh)
            this.refresh(true);
    },

    // remove all layers from the map
    removeAllLayers: function(refresh) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.RemoveAllLayers(this.key, name));

        if (refresh)
            this.refresh(true);
    },

    // show or hide the specified layer on the map
    displayLayer: function(layer, visible, refresh, notify) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.DisplayLayer(this.key, layer, visible));

        if (refresh)
            this.refresh(true);
    },

    // add the specified layer to the selectable layers list
    addSelectableLayer: function(layer) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddSelectableLayer(this.key, layer));
    },

    // remove the specified layer from the selectable layers list
    removeSelectableLayer: function(layer) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.RemoveSelectableLayer(this.key, layer));
    },

    // add the specified layer to the information layers list
    addInformationLayer: function(layer) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddInformationLayer(this.key, layer));
    },

    // remove the specified layer from the information layers list
    removeInformationLayer: function(layer) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.RemoveInformationLayer(this.key, layer));
    },

    // create and add the specified feature layer to the map
    addFeatureLayer: function(layer, table, toolSettings, refresh) {
        this._clear();

        if (toolSettings == true) {
            refresh = Boolean(toolSettings);
            toolSettings = null;
        }

        if (toolSettings == null)
            toolSettings = new Mapping.LayerToolInformation();

        this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddFeatureLayerTools(this.key, table, layer, toolSettings.selectable, toolSettings.displayInformation));

        if (refresh)
            this.refresh(true);
    },

    // create and add the specified label layer to the map
    addLabelLayer: function(layer, table, column, refresh) {
        this._clear();

        if (column == true || column == false && refresh == null) {
            refresh = column;
            column = null;
        }

        if (column != null)
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddLabelLayerColumn(this.key, table, layer, column));
        else
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddLabelLayer(this.key, table, layer));

        if (refresh)
            this.refresh(true);
    },

    // create and add the specified feature layer to the map
    addFeatureLayerStyle: function(layer, table, style, toolSettings, refresh) {
        this._clear();

        if (toolSettings == true) {
            refresh = Boolean(toolSettings);
            toolSettings = null;
        }

        if (toolSettings == null)
            toolSettings = new Mapping.LayerToolInformation();

        this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddFeatureLayerToolsStyle(this.key, table, layer, toolSettings.selectable, toolSettings.displayInformation, style));

        if (refresh)
            this.refresh(true);
    },

    // create and add the specified label layer to the map
    addLabelLayerStyle: function(layer, table, style, column, refresh) {
        this._clear();

        if (column == true || column == false && refresh == null) {
            refresh = column;
            column = null;
        }

        if (column != null)
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddLabelLayerColumnStyle(this.key, table, layer, column, style));
        else
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddLabelLayerStyle(this.key, table, layer, style));

        if (refresh)
            this.refresh(true);
    },

    // pan the map by the specified amount
    pan: function(x, y, refresh) {
        this._clear();
        var eventName = this.id + "_MapPanServerEvent";

        if (!this.serverEvents.contains(eventName)) {
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetPan(this.key, x, y));

            if (refresh)
                this.refresh(true);
        }
        else {
            var event = this.serverEvents.get(eventName);
            event.data = x + "," + y;
            event.fire();
        }
    },

    // set the size of the map on the mapping service.
    setSize: function(width, height) {
        this._clear();

        if (width == null || height == null)
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSize(this.key, this.width, this.height));
        else
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSize(this.key, width, height));
    },

    // get the center of the map
    getCenter: function() {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetCenter(this.key));
    },

    // center the map on a screen point
    center: function(point, refresh) {
        this._clear();
        var eventName = this.id + "_MapCenterServerEvent";

        if (!this.serverEvents.contains(eventName)) {
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetCenter(this.key, point));

            if (refresh)
                this.refresh(true);
        }
        else {
            var event = this.serverEvents.get(eventName);
            event.data = point;
            event.fire();
        }
    },

    // center the map on a map coordinate
    centerMap: function(point, refresh) {
        this._clear();
        var eventName = this.id + "_MapCenterServerEvent";

        if (!this.serverEvents.contains(eventName)) {
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetCenterMap(this.key, point));

            if (refresh)
                this.refresh(true);
        }
        else {
            var event = this.serverEvents.get(eventName);
            event.data = "m," + point;
            event.fire();
        }
    },

    // zoom to a specified distance
    zoom: function(distance, refresh) {
        this._clear();
        var eventName = this.id + "_MapZoomServerEvent";

        if (this.useTiles)
            Core.Debug.write("WARNING: free zoom was used while the map is in tile mode - this may cause unexpected results.");

        if (!this.serverEvents.contains(eventName)) {
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetZoom(this.key, distance));

            if (refresh)
                this.refresh(true);
        }
        else {
            var event = this.serverEvents.get(eventName);
            event.data = "d," + distance;
            event.fire();
        }
    },

    // zoom to a specified distance in units
    zoomUnit: function(distance, unit, refresh) {
        this._clear();
        var eventName = this.id + "_MapZoomServerEvent";

        if (this.useTiles)
            Core.Debug.write("WARNING: free zoom was used while the map is in tile mode - this may cause unexpected results.");

        if (!this.serverEvents.contains(eventName)) {
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetZoomUnit(this.key, distance, unit));

            if (refresh)
                this.refresh(true);
        }
        else {
            var event = this.serverEvents.get(eventName);
            event.data = "d," + distance + "," + unit;
            event.fire();
        }
    },

    // zoom on a point
    zoomPoint: function(mode, factor, point, refresh) {
        this._clear();
        var eventName = this.id + "_MapZoomServerEvent";

        if (this.useTiles)
            Core.Debug.write("WARNING: free zoom was used while the map is in tile mode - this may cause unexpected results.");

        if (!this.serverEvents.contains(eventName)) {
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetZoomPoint(this.key, mode == Mapping.ZoomOptions.ZoomIn, factor, point));

            if (refresh)
                this.refresh(true);
        }
        else {
            var event = this.serverEvents.get(eventName);
            event.data = (mode == Mapping.ZoomOptions.ZoomIn ? "in" : "out") + "," + factor + "," + point;
            event.fire();
        }
    },

    // zoom to a rectangle
    zoomRectangle: function(mode, left, top, right, bottom, refresh) {
        this._clear();
        var eventName = this.id + "_MapZoomServerEvent";

        if (this.useTiles)
            Core.Debug.write("WARNING: free zoom was used while the map is in tile mode - this may cause unexpected results.");

        if (!this.serverEvents.contains(eventName)) {
            this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetZoomRectangle(this.key, mode == Mapping.ZoomOptions.ZoomIn, left, top, right, bottom));

            if (refresh)
                this.refresh(true);
        }
        else {
            var event = this.serverEvents.get(eventName);
            event.data = (mode == Mapping.ZoomOptions.ZoomIn ? "in" : "out") + "," + left + "," + top + "," + right + "," + bottom;
            event.fire();
        }
    },

    // set the zoom level.
    setZoomLevel: function(zoomLevel) {
        if (zoomLevel != null) this.zoomLevel = zoomLevel;
        var zoomDistance = (this.width / this.tileSize) * Mapping.Map.zoomScale[this.zoomLevel];
        Core.Debug.write("Zooming to " + zoomDistance + "m per screen");

        this.zoomUnit(zoomDistance, Mapping.DistanceUnit.Meter);
    },

    // get the current zoom level in map zoom units
    getZoom: function() {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetZoom(this.key));
    },

    // select any selectable map features at the specified point
    selectPoint: function(point, mode, tolerance, refresh) {
        if (tolerance == true) {
            refresh = Boolean(tolerance);
            tolerance = null;
        }

        this._clear();
        var eventName = this.id + "_MapSelectionServerEvent";

        if (!this.serverEvents.contains(eventName)) {
            var selection = null;

            if (tolerance) {
                if (mode == Mapping.SelectionType.AddTo)
                    this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelectionAddTolerance(this.key, point, tolerance, false));
                else if (mode == Mapping.SelectionType.RemoveFrom)
                    this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelectionRemoveTolerance(this.key, point, tolerance, false));
                else
                    this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelectionTolerance(this.key, point, tolerance, false));
            }
            else {
                if (mode == Mapping.SelectionType.AddTo)
                    this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelectionAdd(this.key, point, false));
                else if (mode == Mapping.SelectionType.RemoveFrom)
                    this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelectionRemove(this.key, point, false));
                else
                    this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelection(this.key, point, false));
            }

            if (refresh)
                this.refresh(true);
        }
        else {
            var event = this.serverEvents.get(eventName);
            event.data = (mode == Mapping.SelectionType.AddTo ? "c" : "") +
				(mode == Mapping.SelectionType.RemoveFrom ? "s" : "") +
				"," + point + (tolerance ? ",t=" : "");
            event.fire();
        }
    },

    // select and return any selectable map features at the specified point
    selectPointFeatures: function(point, mode, tolerance, refresh) {
        if (tolerance == true) {
            refresh = Boolean(tolerance);
            tolerance = null;
        }

        this._clear();
        var eventName = this.id + "_MapSelectionServerEvent";

        if (!this.serverEvents.contains(eventName)) {
            var selection = null;

            if (tolerance) {
                if (mode == Mapping.SelectionType.AddTo)
                    selection = this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelectionAddTolerance(this.key, point, tolerance, true));
                else if (mode == Mapping.SelectionType.RemoveFrom)
                    selection = this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelectionRemoveTolerance(this.key, point, tolerance, true));
                else
                    selection = this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelectionTolerance(this.key, point, tolerance, true));
            }
            else {
                if (mode == Mapping.SelectionType.AddTo)
                    selection = this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelectionAdd(this.key, point, true));
                else if (mode == Mapping.SelectionType.RemoveFrom)
                    selection = this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelectionRemove(this.key, point, true));
                else
                    selection = this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetSelection(this.key, point, true));
            }

            if (refresh)
                this.refresh(true);

            return selection;
        }
        else {
            var event = this.serverEvents.get(eventName);
            event.data = (mode == Mapping.SelectionType.AddTo ? "c" : "") +
				(mode == Mapping.SelectionType.RemoveFrom ? "s" : "") +
				"," + point + (tolerance ? ",t=" : "");
            event.fire();
        }
    },

    // search for any selectable map features at the specified point
    searchPoint: function(point, tolerance) {
        this._clear();
        var eventName = this.id + "_MapSearchServerEvent";

        if (!this.serverEvents.contains(eventName)) {
            if (tolerance)
                return this._get(Torbay.GIS.Web.Controls.MappingControlActions.SearchPointTolerance(this.key, point, tolerance));
            else
                return this._get(Torbay.GIS.Web.Controls.MappingControlActions.SearchPoint(this.key, point));
        }
        else {
            var event = this.serverEvents.get(eventName);
            event.data = point;
            event.fire();
        }
    },

    // get any current map selection
    getSelection: function() {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetSelection(this.key));
    },

    // clear any current map selection
    clearSelection: function(refresh) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.ClearSelection(this.key));

        if (refresh)
            this.refresh(true);
    },

    // draw the specified text on the map
    drawText: function(point, text, refresh) {
        this._clear();
        var eventName = this.id + "_MapDrawServerEvent";

        if (!this.serverEvents.contains(eventName)) {
            var id = this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddText(this.key, text, point));

            if (refresh)
                this.refresh(true);

            return id;
        }
        else {
            // t,x,y,text
            var event = this.serverEvents.get(eventName);
            event.data = "t," + point + "," + text;
            event.fire();
        }
    },

    // draw the specified text on the map with the specified options
    drawTextOptions: function(point, text, size, color, backColor, haloColor, fontName, bold, italics, underline, refresh) {
        this._clear();
        var eventName = this.id + "_MapDrawServerEvent";

        if (!this.serverEvents.contains(eventName)) {
            var id = this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddTextXYSizeColorsFont(this.key, text, point, size,
				color.toArgb(), backColor.toArgb(), haloColor.toArgb(), fontName, bold, italics, underline));

            if (refresh)
                this.refresh(true);

            return id;
        }
        else {
            // t,x,y,text,size,color,backcolor,halocolor,font,bold,italics,underline,layer
            var event = this.serverEvents.get(eventName);
            event.data = "t," + point + "," + text + "," + size + "," +
					color.toArgb() + "," + backColor.toArgb() + "," + haloColor.toArgb() + "," +
					fontName + "," + bold + "," + italics + "," + underline;
            event.fire();
        }
    },

    // draw the specified symbol on the map
    drawSymbol: function(point, symbol, size, color, backColor, haloColor, refresh) {
        this._clear();
        var eventName = this.id + "_MapDrawServerEvent";

        if (!this.serverEvents.contains(eventName)) {
            var id = this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddSymbolXYSizeColors(this.key, point,
				symbol, size, color.toArgb(), backColor.toArgb(), haloColor.toArgb()));

            if (refresh)
                this.refresh(true);

            return id;
        }
        else {
            // p,x,y,symbol,size,color,backcolor,halocolor
            var event = this.serverEvents.get(eventName);
            event.data = "p," + point + "," + symbol + "," + size + "," +
					color.toArgb() + "," + backColor.toArgb() + "," + haloColor.toArgb();
            event.fire();
        }
    },

    // draw the specified Bitmap symbol on the map. Added by Sean Schofield
    drawBitmapSymbol: function(point, bmpName, size, color, backColor, haloColor, refresh) {
        this._clear();
        var eventName = this.id + "_MapDrawServerEvent";

        if (!this.serverEvents.contains(eventName)) {
            var symbol = new Mapping.BitmapMapSymbol(bmpName, "", color, backColor, haloColor, point.toMap(this), size);
            symbol.id = this._get(Torbay.GIS.Web.Controls.MappingControlActions.AddSymbol(this.key, symbol));
            if (refresh)
                this.refresh(true);
            //alert(symbol.toJSON());
            return symbol.id;
        }
        else {
            // p,x,y,symbol,size,color,backcolor,halocolor
            //			var event = this.serverEvents.get(eventName);
            //			event.data = "p," + point + "," + symbol + "," + size + "," + 
            //					color.toArgb() + "," + backColor.toArgb() + "," + haloColor.toArgb();
            //			event.fire();
        }
    },

    // gets specified symbol from map.  Added by Sean Schofield
    getSymbol: function(symbolId) {
        this._clear();
        var symbol = this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetSymbol(this.key, symbolId));
        return symbol;
    },

    // remove the specified symbol on the map
    removeSymbol: function(symbolId, refresh) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.RemoveSymbolById(this.key, symbolId));

        if (refresh)
            this.refresh(true);
    },

    // close all the tables in the map
    closeAllTables: function(refresh) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.CloseAllTables(this.key));

        if (refresh)
            this.refresh(true);
    },

    // close the specified tables in the map
    closeTable: function(table, refresh) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.CloseTable(this.key, table));

        if (refresh)
            this.refresh(true);
    },

    // clear the workspace and close any open tables
    clearWorkspace: function(refresh) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.ClearWorkspace(this.key));

        if (refresh)
            this.refresh(true);
    },

    // get the workspace xml as a string
    getWorkspaceXml: function() {
        this._clear();
        return this._get(Torbay.GIS.Web.Controls.MappingControlActions.GetWorkspaceXml(this.key));
    },

    // set the workspace xml to the specified string
    setWorkspaceXml: function(xml, refresh) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.SetWorkspaceXml(this.key, xml));

        if (refresh)
            this.refresh(true);
    },

    // load the specified workspace
    loadWorkspace: function(path, refresh) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.LoadWorkspace(this.key, path));

        if (refresh)
            this.refresh(true);
    },

    // save the workspace to the specified file
    saveWorkspace: function(path) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.SaveWorkspace(this.key, path));
    },

    // open a table into the map from the specified path 
    openTablePath: function(path, name) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.OpenTablePath(this.key, path, name));
    },

    // open a table into the map from a SQL Server database  
    openTableSqlServerODBC: function(name, connectionString, query) {
        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.OpenTableSqlServer(this.key, name, connectionString, query));
    },

    // open a table into the map from a SQL Server database 
    openTableSqlServer: function(name, server, database, query) {
        var connectionString = "Driver={SQL Server};Server=" + server + ";Database=" + database + ";Trusted_Connection=yes";

        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.OpenTableSqlServer(this.key, name, connectionString, query));
    },

    // open a table into the map from a SQL Server database 
    openTableSqlServerUser: function(name, server, database, user, password, query) {
        var connectionString = "Driver={SQL Server};Server=" + server + ";Trusted_Connection=no;Database=" + database + ";Uid=" + user + ";Pwd=" + password;

        this._clear();
        this._get(Torbay.GIS.Web.Controls.MappingControlActions.OpenTableSqlServer(this.key, name, connectionString, query));
    },

    // get the x coordinate of the mouse relative to the map
    mouseX: function() {
        var adjustment = 0;
        if (Core.Environment.browser == Core.Browser.IE && Core.Environment.platform == Core.Platform.Windows)
            adjustment -= 2;

        return Core.Utility.getClientX() + Core.Utility.getClientLeft() - this.left + adjustment;
    },

    // get the y coordinate of the mouse relative to the map
    mouseY: function() {
        var adjustment = 0;
        if (Core.Environment.browser == Core.Browser.IE && Core.Environment.platform == Core.Platform.Windows)
            adjustment -= 2;

        return Core.Utility.getClientY() + Core.Utility.getClientTop() - this.top + adjustment;
    },

    // reset the map image to fill the map container
    resetPosition: function() {
        this.image.style.left = Core.Utility.toSize(0);
        this.image.style.top = Core.Utility.toSize(0);
        this.image.style.clip = "rect(0 " + this.width + "px " + this.height + "px 0)";

        // this.preCacheSize
        //this.image.style.left = Core.Utility.toSize(-this.preCacheSize);
        //this.image.style.top = Core.Utility.toSize(-this.preCacheSize);
        //this.image.style.width = Core.Utility.toSize(this.width + (this.preCacheSize * 2));
        //this.image.style.height = Core.Utility.toSize(this.height + (this.preCacheSize * 2));
        //this.image.style.clip = "rect(" + this.preCacheSize + " " + this.width + " " + this.height + " " + this.preCacheSize + ")";
    },

    // event handlers
    _loadFromCache: function() {
        this.image.onload = this._resetPosition.bindAsEventListener(this);
        this.image.src = this.cachedImage.src;
    },

    _resetPosition: function() {
        if (!this.useTiles)
            this.resetPosition();

        Drawing.Window.setCursor(this._cursor, this.image);

        if (this.onloaded)
            this.onloaded();

        if (this.showLoading)
            this.hideMessage("map");
    },

    toQuadtree: function(x, y, z) {
        var qtree = "t";
        var m = new Array(['t', 'q'], ['s', 'r']);

        x = Math.round(x);
        y = Math.round(y);

        // running offsets
        var ox = 0, oy = 0;

        for (var i = 1; i < z; i++) {
            var col = (x + 0.001 > Mapping.Map.zoomScale[i + 1] + ox) ? 1 : 0;
            var row = (y + 0.001 > Mapping.Map.zoomScale[i + 1] + oy) ? 1 : 0;

            if (col > 0)
                ox += Mapping.Map.zoomScale[i + 1];

            if (row > 0)
                oy += Mapping.Map.zoomScale[i + 1];

            qtree += m[col][row];
        }

        return qtree;
    },

    toXYZoom: function(qtree) {
        var x = 0, y = 0, z = 1;
        var m = { 'q': [0, 1], 't': [0, 0], 'r': [1, 1], 's': [1, 0] };

        // ignore the first quad reference, which will be a 't'.
        for (var k = 1; k < qtree.length; k++) {
            var c = qtree.substr(k, 1);
            z++;
            x += Math.round(m[c][0] * Mapping.Map.zoomScale[z]);
            y += Math.round(m[c][1] * Mapping.Map.zoomScale[z]);
        }

        return { x: x, y: y, z: z };
    },

    moveQuadrant: function(qtree, moveX, moveY) {
        var bits = this.getQuadBits(qtree);
        var x = "", y = "";

        for (var i = 0; i < bits.length; i++) {
            x += String(bits[i][0]);
            y += String(bits[i][1]);
        }

        var xb10 = Number(Core.Utility.convertBase(x, 2, 10));
        var xp1 = xb10 + moveX;
        var xp1b2 = Core.Utility.toBinary(Core.Utility.convertBase(xp1, 10, 2), bits.length);

        var yb10 = Number(Core.Utility.convertBase(y, 2, 10));
        var yp1 = yb10 + moveY;
        var yp1b2 = Core.Utility.toBinary(Core.Utility.convertBase(yp1, 10, 2), bits.length);

        //alert("X: " + x + " in b10 = " + xb10 + ", to " + xp1 + " in b2 = \t" + xp1b2 + "\nY: " + y + " in b10 = " + yb10 + ", to " + yp1 + " in b2 = \t" + yp1b2);

        x = String(xp1b2);
        y = String(yp1b2);
        qtree = "";

        for (var i = 0; i < bits.length; i++) {
            var xb = Number(x.substr(i, 1));
            var yb = Number(y.substr(i, 1));

            // right bit = lr, left bit = ud
            qtree += Mapping.Map.quadFromBit[yb][xb];
        }

        return qtree;
    },

    getQuadBits: function(qtree) {
        var bits = new Array();

        for (var k = 0; k < qtree.length; k++) {
            var c = qtree.substr(k, 1);
            // right bit = lr, left bit = ud
            bits[bits.length] = [Mapping.Map.quadRightBit[c], Mapping.Map.quadLeftBit[c]];
        }

        return bits;
    }
},
{
    quadLeftBit: { 't': 1, 'q': 0, 's': 1, 'r': 0 },
    quadRightBit: { 't': 0, 'q': 0, 's': 1, 'r': 1 },
    quadFromBit: new Array(['q', 'r'], ['t', 's']),

    // zoomScale: new Array(-1, 1600000, 800000, 400000, 200000, 100000, 50000, 25000, 12500, 6250, 3125,
    //							1562.5, 781.25, 390.625, 195.3125, 97.65625, 48.828125, 24.4140625)
    //zoomScale: new Array(-1, 1638400, 819200, 409600, 204800, 102400, 51200, 25600, 12800, 6400, 3200,
	//							1600, 800, 400, 200, 100, 50, 25)
    zoomScale: new Array(-1, 2097152, 1048576, 524288, 262144, 131072, 65536, 32768, 16384, 8192, 4096, 2048, 1024, 512, 256, 128,
								64, 32, 16, 8, 4, 2, 1)
});

////////////////////////////////////////////////////////////////////////////////////////////

Mapping.DoublePoint = Class.create();
Mapping.DoublePoint.prototype =
{
	initialize: function(x, y)
	{
		this.X = this.Y = 0;

		if (x) this.X = x;
		if (y) this.Y = y;
	},
	
	toScreen: function(map)
	{
		return map.convertMapToScreenCoordinates(this);
	},
	
	toString: function()
	{
		return this.X + ", " + this.Y;
	},
	
	toJSON: function()
	{
		return "{\"__type\":\"Torbay.GIS.MappingService.DoublePoint\",\"X\":" + this.X + ",\"Y\":" + this.Y + "}";
	}
};

////////////////////////////////////////////////////////////////////////////////////////////

Mapping.IntegerPoint = Class.create();
Mapping.IntegerPoint.prototype =
{
	initialize: function(x, y)
	{
		this.X = this.Y = 0;

		if (x) this.X = x;
		if (y) this.Y = y;
	},
	
	toMap: function(map)
	{
		return map.convertScreenToMapCoordinates(this);
	},
	
	toString: function()
	{
		return this.X + ", " + this.Y;
	},
	
	toJSON: function()
	{
		return "{\"__type\":\"Torbay.GIS.MappingService.IntegerPoint\",\"X\":" + Math.ceil(this.X) + ",\"Y\":" + Math.ceil(this.Y) + "}";
	}
};

////////////////////////////////////////////////////////////////////////////////////////////

Mapping.BoundsRectangle = Class.create();
Mapping.BoundsRectangle.prototype =
{
	initialize: function(minX, maxX, minY, maxY)
	{
		this.minX = this.maxX = this.minY = this.maxY = 0;

		if (minX) this.minX = minX;
		if (maxX) this.maxX = maxX;
    	if (minY) this.minY = minY;
		if (maxY) this.maxY = maxY;

	},
	
	toString: function()
	{
		return this.minX + ", " + this.maxX + ", " + this.minY + ", " + this.maxY;
	},
	
	toJSON: function()
	{
		return "{\"__type\":\"Torbay.GIS.MappingService.BoundsRectangle\",\"minX\":" + this.minX + ",\"maxX\":" + this.maxX + ",\"minY\":" + this.minY + ",\"maxY\":" + this.maxY +"}";
	}
};





