
// ----------- Global variables and functions --------

var g_blank = 'images/blank.gif';

/** 
 * Global set of dynamically generated maps within the document
 * @type Array
 */
var g_maps = new Array();

/** 
 * Global set of categories within the document, indexed by category id
 * @type Array
 */
var g_categories = new Array();

/**
 * Global set of layer groups, indexed by layergroup gid. 
 * DO NOT USE DIRECTLY - use function lookupLayerGroup(id) instead.
 * @type Array
 */
var g_layerGroups = new Array();

/**
 * Global set of layers, indexed by layer gid.
 * DO NOT USE DIRECTLY - use function lookupLayer(id) instead.
 * @type Array
 */
var g_layers = new Array();

/** Global counter for a map (LayerGroup) z-index. It is increased on
 * every init of LayerGroup object.
 * @type int
 */
var g_mapZindex = 10;

/** Global counter for layer id
 * @type int
 */
var g_layerIndex = 0;

/** Global counter for layer group id
 * @type int
 */
var g_layerGroupIndex = 0;

/** An global array of layer groups, sorted by z-index. Layer groups
 * with attribute fixedOrder set is not included in array. This variable
 * is null until set by menu page <i>Layer order</i>.
 * @type Array
 */
var g_orderedLayerGroups;

/** Find a layer group with specifed id from global layer group array. Use this
 * function instead of referencing array directly.
 * @param {int} id the layer group gid
 * @return layer group found, false otherwise
 */
function lookupLayerGroup(id){
    for(var i = 0 ; i < g_layerGroups.length ; i++){
        if( id == g_layerGroups[i].gid ){
            return g_layerGroups[i];
        }
    }
    return false;
}

/** Find a layer with specifed id from global layer array. Use this
 * function instead of referencing array directly.
 * @param {int} id the layer gid
 * @return layer found, false otherwise
 */
function lookupLayer(id){
    for(var i = 0 ; i < g_layers.length ; i++){
        if( id == g_layers[i].gid ){
            return g_layers[i];
        }
    }
    return false;
}

/** Delete a layer from an array by returning an copy of the array excluding
 * deleted layer.
 * @param {int} layerId gid for layer to delete
 * @param {Array} layerArray array of layers
 * @return new array which is a copy of layerArray without deleted layer
 */
function deleteLayer(layerId, layerArray){
    var tmpArr = new Array();
    for(var i = 0 ; i < layerArray.length ; i++){
        if( layerId != layerArray[i].gid ){
            tmpArr[tmpArr.length] = layerArray[i];
        }
    }
    return tmpArr;
}

/**
 * Find a layer's layergroup
 * @param {Layer} layerToFind the layer to look for
 * @return the LayerGroup containing specified layer, false if not found
 * @type LayerGroup
 */
function getLayerGroupForLayer(layerToFind){
    for( var i = 0; i< g_layerGroups.length; i++){
        var layerGroup = g_layerGroups[i];
        for( var j = 0; j < layerGroup.layers.length; j++){
            var layer = layerGroup.layers[j];
            if( layer.gid == layerToFind.gid ){
                return layerGroup;
            }
        }
    }
    return false;
}

/** Fix all categories allChecked attribute. Attr is set to 
 * true if all contained layers are visible. This function
 * is only needed to be called at client startup.
 */
function fixCategoryVisibility(){
    for(var i = 0; i < g_categories.length; i++){
        var category = g_categories[i];
        var allSelected = true;
        for(var j = 0; j < category.layers.length; j++){
            var layer = category.layers[j];
            if( !layer.visible ){
                allSelected = false;
                break;
            }
        }
        category.allChecked = allSelected;
    }
}

/** Sort all layergroups layers. This function
 * is only needed to be called at client startup.
 */
function fixLayerOrdering(){
    for(var i = 0; i < g_layerGroups.length; i++){
        var layGrp = g_layerGroups[i];
        layGrp.layers.sort(compareLayer);        
        for(var j = 0 ; j < layGrp.layers.length; j++){
            var layer = layGrp.layers[j];
            layer.order = layGrp.layers.length-j;
        }
    }
}

/** Get an array with layergroups that is ordered according to
 * z-index. Layer groups that has attribute fixedOrder = true is excluded
 * (eg. overview layergroups).
 * @return an ordered array with layer groups
 * @type Array
 */
function getOrderedLayerGroups(){
    var copy = new Array();
    var index = 0;
    for(var i = 0; i < g_layerGroups.length ; i++){
        var layGrp = g_layerGroups[i];
        if( !layGrp.fixedOrder ){
            copy[index++] = layGrp;
        }
    }

    copy.sort(compareLayerGroup);
    return copy;
}

/** Move a layer group up, i.e increase the z-index for layer group and
 * corresponding image div's.
 * @param {int} gid id for layer group to be moved 
 */
function moveLayerGroupUp(gid){
    var prevZindex = -1;
    var prevIndex = -1;

    for(var i = 0 ; i < g_orderedLayerGroups.length; i++){
        var grp = g_orderedLayerGroups[i];

        if(grp.hasVisibleLayer() && grp.gid!=gid){
            prevZindex = grp.zIndex;
            prevIndex = i;
        }
        if( grp.gid == gid && prevIndex!=-1){
            grp.zIndex = prevZindex;
            // adjust zindex on layergroups that follows
            for(var j = prevIndex; j < g_orderedLayerGroups.length; j++){
                if(j!=i){
                    var g = g_orderedLayerGroups[j];
                    g.zIndex = --prevZindex;
                }
            }
            break;
        }
    }
        
    fixZindexOnDivs(g_orderedLayerGroups);
}

/** Move a layer group down, i.e decreasee the z-index for layer group and
 * corresponding image div's.
 * @param {int} gid id for layer group to be moved 
 */
function moveLayerGroupDown( gid){
    var prevZindex = -1;
    var prevIndex = -1;

    for(var i = g_orderedLayerGroups.length-1 ; i >=0 ; i--){
        var grp = g_orderedLayerGroups[i];

        if(grp.hasVisibleLayer() && grp.gid!=gid){
            prevZindex = grp.zIndex;
            prevIndex = i;
        }
        if( grp.gid == gid && prevIndex!=-1){
            grp.zIndex = prevZindex;
            for(var j = prevIndex; j >=0; j--){
                if(j!=i){
                    var g = g_orderedLayerGroups[j];
                    g.zIndex = ++prevZindex;
                }
            }
            break;
        }
    }
        
    fixZindexOnDivs(g_orderedLayerGroups);
}

/** Set z-index on layer group div's
 * @param {Array} grpArr sorted array with LayerGroup objects
 */
function fixZindexOnDivs(grpArr){
    for(var i = 0 ;i < grpArr.length; i++){
        var grp = grpArr[i];
        var div = document.getElementById('map_g0_'+grp.gid);
        if(div){
            div.style.zIndex = grp.zIndex;
        }
        else{
            alert('Can\'t find div map_g0_'+grp.gid+' ('+grp.title+')');
        }
    }
}

/** Create a new Layer object. Use this function to create layer 
 * objects from other browser windows. For spec of params see the Layer object.
 * @return a new layer
 * @type Layer
 */
function getNewLayerObj(name,title, visible, displayName, order, style, wfsQueryable){
   
    var layer = new Layer(name,title, visible, displayName, order, style, wfsQueryable);
    return layer;
}

/** Creates a new LayerGroup object. Use this function to create layergroup 
 * objects from other browser windows. For spec of params see the LayerGroup object.
 * @return a new layergroup
 * @type LayerGroup
 */
function getNewLayerGroupObj(title, transparent, format, infoFormat, subtype, sldName, dataSourceName, wmsURL, wfsURL, parentMap, wmsVer){

    var layerGrp = new LayerGroup(title, transparent, format, infoFormat, subtype, sldName, dataSourceName, wmsURL, wfsURL, parentMap, wmsVer);    
    return layerGrp;
}

// ----------- END Global variables and functions --------

// ======================================== Epsg

/**
 * @constructor
 * @param {int} value the epsg-value
 * @param {String} description a textual description
 * @param {String} unit type of unit, ie degree or meter
 */
function Epsg(value, description, unit){
    /**
     * The epsg value
     * @type int
     */ 
    this.value = value;

    /**
     * A description of the epsg
     * @type String
     */
    this.description = description;

    /**
     * Type of unit, degree or meter
     * @type String
     */
    this.unit = unit;
}

// ======================================== Overview

/**
 * Object representing an overview map
 * @constructor
 * @param {Dimension} dimension the dimensions of the overview map
 * @param {Extent} extent the extent for the overview map
 * @param {Epsg} epsg the Epsg object
 */
function Overview(dimension, extent, epsg){
  /** @type Dimension */
  this.dimension = dimension;
  /** @type Extent */
  this.cBounds = extent.copy();
  /** @type Epsg */
  this.epsg = epsg;
  /** @type Array */
  this.groups = new Array();

  this.addLayerGroup = ov_addLayerGroup;
  this.DDtoPixel = ov_DDtoPixel;
}

/**
 * Add a layergroup to this overview. The layer group attribute
 * fixedOrder is set to true and zIndex is increased with 50.
 * @param {LayerGroup} layerGroup the LayerGroup to be added
 */
function ov_addLayerGroup(layerGroup){
  layerGroup.fixedOrder = true;
  layerGroup.zIndex += 50;
  this.groups[ this.groups.length ] = layerGroup;
}

/**
 * Converts decimal degree coordinates to pixel coordinates
 * @param lon the longitude
 * @param lat the latitude
 * @return the pixel coordinates
 * @type Coordinate
 */
function ov_DDtoPixel(lon, lat){
    var x;
    var y;

    if(this.epsg.unit == "degree"){
        x = ( (lon + 180 ) * (this.dimension.width/360) );
        y = ((( lat * -1) + 90) * (this.dimension.height/180));
    }
    else{
    	x = Math.abs( (lon - this.cBounds.xMin) * (this.dimension.width/this.cBounds.getDX()));
    	y = Math.abs( (lat - this.cBounds.yMax) * (this.dimension.height/this.cBounds.getDY()));        
    }
   
    return new Coordinate(x,y);
}

// ======================================== Category

/** Object representing feature categories. This 
 * constructor adds this object to global
 * <code>g_categories</code> array.
 * @constructor
 * @param {String} title the category title
 */
function Category(title){
    var numCat = g_categories.length;

    /** @type int */
    this.id = numCat;
    /** @type String */
    this.title = title;
    /** @type Array */
    this.layers = new Array();
    /** @type boolean */
    this.allChecked = false;

    this.addLayer = cat_addLayer;
   
    g_categories[ numCat ] = this;
}

/** Add a layer to this category
 * @param {Layer} layer the Layer to be added
 */
function cat_addLayer(layer){    
    this.layers[ this.layers.length ] = layer;
}

// ======================================== ZoomExtent

/** An extent with a name (region)
 * @constructor
 * @param {String} region a name of this zoom extent
 * @param {Extent} extent the extent
 */
function ZoomExtent( region, extent){
  /** @type String */
  this.region = region;
  /** @type Extent */
  this.extent = extent;
}

// ======================================== ScaleExtent

/** An extent with a name (scale)
 * @constructor
 * @param {String} name of this scale extent
 * @param {Extent} scale of this the extent
 */
function ScaleExtent( name, scale){
  /** @type String */
  this.name = name;
  /** @type Extent */
  this.scale = scale;
}


// ======================================== Dimension

/** The Dimension object encapsulates the width and height. This object
 * also has a <code>aspect</code> attribute that is calculated from
 * width / height
 * @constructor
 * @param {int} width the width value
 * @param {int} height the height value
 */
function Dimension(width, height){
  /** @type int */
  this.width = width;
  /** @type int */
  this.height = height;

  /** The relatioship between width / height
   * @type double
   */
  this.aspect = width / height;
}

// ======================================== Scale

/** Object representing a scale. 
 * @constructor
 * @param {int} scale
 */
function Scale(scale){
  /** @type int */
  this.scale = scale
  
  this.toString = sc_toString;
}

function sc_toString(){
    return ''+this.scale;
}


// ======================================== Extent

/** Object representing an extent. 
 * @constructor
 * @param {int} xMin
 * @param {int} yMin
 * @param {int} xMax
 * @param {int} yMax
 */
function Extent(xMin, yMin, xMax, yMax){
  /** @type int */
  this.xMin = xMin;
  /** @type int */
  this.yMin = yMin;
  /** @type int */
  this.xMax = xMax;
  /** @type int */
  this.yMax = yMax; 
    
  this.copy = ex_copy;    
  this.getDX = ex_getDX; 
  this.getDY = ex_getDY;
  this.toString = ex_toString;
}

function ex_toString(){
    return '('+this.xMin+', '+this.yMin+') ('+this.xMax+', '+this.yMax+')';
}
/** Create a copy of this extent
 * @return an Extent object that has same values as this extent
 * @type Extent
 */
function ex_copy(){
   var ex = new Extent( this.xMin, this.yMin, this.xMax, this.yMax);
   return ex;
}

/** Calculate dx, ie xMax - xMin
 * @return the dx, ie the x difference
 * @type double
 */
function ex_getDX(){
    return this.xMax - this.xMin;
}

/** Calculate dy, ie yMax - yMin
 * @return the dy, ie the y difference
 * @type double
 */
function ex_getDY(){
    return this.yMax - this.yMin;
}

// ======================================== LayerName

/** Name of a Layer object.
 * @constructor
 * @param {String} name layer name
 * @param {String} style
 * @param {boolean} wfsQueryable true/false if queryable
 */
function LayerName(name, style, wfsQueryable){
    /** @type String */
    this.name = name;
    /** @type String */
    this.style = style;
    /** @type boolean */
    this.wfsQueryable = wfsQueryable;
}

// ======================================== Layer

/** A layer. All layers must be contained by a LayerGroup or Category.
 * Upon instantiation, this layer is added to the global g_layers array.
 * <p>A layer has name, style and wfsQueryable attributes set if it
 * doesn't consists of LayerName objects.
 * @constructor
 * @param {String} name 
 * @param {String} title
 * @param {boolean} visible true if layer should be visible, false otherwise
 * @param {String} displayName the String to be shown in gui
 * @param {String} order display order, bigger number = further back
 * @param {String} style
 * @param {boolean} wfsQueryable true if layer is queryable, otherwise false
 */
function Layer(name,title, visible, displayName, order, style, wfsQueryable){
    /** @type int */
    this.gid = g_layerIndex++;
    /** @type String */
    this.name = name;
    /** @type String */
    this.title = title;
    /** @type boolean */
    this.visible = visible;
    /** @type String */
    this.displayName = displayName;
    /** @type String */
    this.style = style;
    /** @type int */
    this.order = order;
    /** @type boolean */
    this.wfsQueryable = wfsQueryable;
    /** @type Array */
    this.nameArr = new Array();
      
    this.hasName = la_hasName;
    this.hasNameStr = la_hasNameStr;
    this.addName = la_addName; 
    this.getNames = la_getNames;
    this.getStyles = la_getStyles;
    this.newStyles = la_newStyles;
    this.isQueryable = la_isQueryable;        

    g_layers[g_layers.length] = this;
}

/** Check if this layer and another layer has a common name
 * @param {Layer} otherLayer the other layer
 * @return true if otherLayer has a name that this layer also has
 * @type boolean
 */
function la_hasName(otherLayer){
    if( this.nameArr.length == 0){
        if( otherLayer.nameArr.length == 0){
            return this.name == otherLayer.name;
        }
        else{
            return strInLayerNameArray(this.name, otherLayer.nameArr );
        }
    }
    else{
        if( otherLayer.nameArr.length == 0){
            return strInLayerNameArray( otherLayer.name, this.nameArr );
        }
        else{
            return commonLayerNameInArrays( this.nameArr, otherLayer.nameArr );
        }
    }
}

/** Check if this layer has a name
 * @param {String} name the name of layer
 * @return true if this layer has specified name, false otherwise
 */
function la_hasNameStr(name){
    if( this.nameArr.length == 0){
        return this.name == name;
    }
    else{
        return strInLayerNameArray(name, this.nameArr );
    }
}

/** Check if this layer has a queryable name
 * @param {String} name the layername 
 * @return true if this layer holds a LayerName or name specified by parameter that is queryable
 * @type boolean
 */
function la_isQueryable(name){
    if( this.nameArr.length == 0){
        if( this.name == name )
            return this.wfsQueryable;
    }
    else{
        for(var i = 0; i < this.nameArr.length; i++){
            var layerName = this.nameArr[i];
            if(layerName.name == name){
                return layerName.wfsQueryable;
            }
    
        }
    }
    return false;
}

/** Get the name(s) for this layer. If only one name, that is returned.
 * If more than one name, a comma separated list of names is returned.
 * @return a comma separated list of names or just the name
 * @type String
 */
function la_getNames(){
  var resNames = '';
  for(var i = 0; i < this.nameArr.length; i++){
    var layerName = this.nameArr[i];
    resNames += layerName.name + ',';
  }

  if( resNames.length > 0){
    // remove last comma
    resNames = resNames.substring(0, resNames.length-1);
  }
  else{
    resNames = this.name;
  }

  return resNames;
}

/** Get the style(s) for this layer. If layer has a list of LayerNames, a comma 
 * separated list of styles is returned. If a LayerName has not style set, use 
 * this layers style attribute.
 * @return a comma separated list of styles or just the layer style
 * @type String
 */
function la_getStyles(){
  var res = '';
  for(var i = 0; i < this.nameArr.length; i++){
    var layerName = this.nameArr[i];    
    if( layerName.style.length > 0){
        res += layerName.style + ',';
    }
    else{
        res += this.style + ',';
    }
  }

  if( res.length > 0){
    // remove last comma
    res = res.substring(0, res.length-1);
  }
  else{
      res = this.style;
  }
  return res;
}

/** Add a layername to this layer's name array.
 * @param {LayerName} name the layer name to be added
 */
function la_addName(name){
    this.nameArr[this.nameArr.length] = name;
}

/** Update the style for this layer's LayerNames with
 * specified name and this layers style attribute if name attribute
 * is same as name parameter.
 * @param {String} name the layername
 * @param {String} newStyle the new style value
 */
function la_newStyles(name,newStyle){
   
    for(var i = 0; i < this.nameArr.length; i++){
        var layerName = this.nameArr[i];    
        if( layerName.name == name){
            layerName.style = newStyle;
        }
    }

    if( this.name == name){
        this.style = newStyle;
    }
}

// ======================================== LayerGroup

/** Group of layers. Upon instantiation, this layergroup is added to 
 * the global g_layerGroups array.
 * @constructor
 * @param {String} title the title of the group
 * @param {boolean} transparent true if map image should be transparent
 * @param {String} format the mimetype of the requested map image, eg image/png
 * @param {String} infoFormat the mimetype of the getFeatureInfo request, eg text/xml
 * @param {String} subtype default ""
 * @param {String} sldName
 * @param {Sring} dataSourceName
 * @param {String} wmsURL
 * @param {String} wfsURL
 * @param {OgcMap} parentMap the OgcMap that owns this layergroup
 * @param {String} WMS version
 */
function LayerGroup(title, transparent, format, infoFormat, subtype, sldName, dataSourceName, wmsURL, wfsURL, parentMap, wmsVer){
    /** @type int */
    this.gid = g_layerGroupIndex++;
    /** @type String */
    this.title = title;
    /** @type boolean */
    this.transparent = transparent;
    /** @type String */
    this.format = format;
    /** @type String */
    this.infoFormat = infoFormat;
    /** @type String */
    this.subtype = subtype;
    /** @type String */
    this.sldName = sldName;
    /** @type String */
    this.dataSourceName = dataSourceName;
    /** @type String */
    this.wmsURL = wmsURL.replace(/&amp;/gi,"&");
    /** @type String */
    this.wfsURL = wfsURL.replace(/&amp;/gi,"&");
    /** @type OgcMap */
    this.parentMap = parentMap;
    /** @type Array */
    this.layers = new Array();
    /** @type Image */
    this.imgTag = new Image();
    /** @type String */
    this.wmsVer = wmsVer;

    /** @type boolean */
    this.loading = false;
    /** @type boolean */
    this.blank = true;
    /** @type boolean */
    this.allChecked = false;
    /** @type int */
    this.zIndex = g_mapZindex++;
    /** @type boolean */
    this.fixedOrder = false;
    /** @type boolean */
    this.isUserDefined = false;

    this.addLayer = lg_addLayer;
    this.getMapUrl = lg_getMapUrl;
    this.getFeatureInfoQuery = lg_getFeatureInfoQuery;
    this.hasVisibleLayer = lg_hasVisibleLayer;
    this.moveLayerUp = lg_moveLayerUp;
    this.moveLayerDown = lg_moveLayerDown;
    this.findLayerByName = lg_findLayerByName;


    // TODO decide about defaults
    if ((!this.format) || (this.format == "undefined"))
          this.format = "image/png";
    if ((!this.sldName) || (this.sldName == "undefined"))
          this.sldName = "undefined";
    if ((!this.dataSourceName) || (this.dataSourceName == "undefined"))
          this.dataSourceName = "undefined";
    if ((!this.wmsVer) || (this.vmsVer == "undefined"))
          this.wmsVer = "1.1.1";

    g_layerGroups[g_layerGroups.length] = this;   
}

function lg_findLayerByName(name){
    var found;
    for( var i = 0; i < this.layers.length ; i++){
       layer = this.layers[i];
       if( layer.hasNameStr( name ) ){
           found = layer;
           break;
       }       
    }
    return found;
}

/** Add a layer to this group
 * @param {Layer} layer the layer to add
 */
function lg_addLayer(layer){
    this.layers[ this.layers.length ] = layer;
}

/** Check if any layer contained in this layergroup is visible
 * @return true if at least one layer is visible, false otherwise
 * @type boolean
 */
function lg_hasVisibleLayer(){
    for( var i = 0; i < this.layers.length ; i++){
       layer = this.layers[i];
       if( layer.visible )
           return true;
    }
    return false;
}

/** Move a layer down, ie increase order value, adjust
 * following layers and resort the layer array.
 * @param {int} gid the layer id
 */
function lg_moveLayerDown(gid){
    var prevOrder = -1;
    var prevIndex = -1;

    for(var i = 0 ; i < this.layers.length; i++){
        var layer = this.layers[i];
        if(layer.visible && layer.gid!=gid){
            prevOrder = layer.order;
            prevIndex = i;
        }
        if( layer.gid == gid && prevIndex!=-1){
            layer.order = prevOrder;
            for(var j = prevIndex; j < this.layers.length; j++){
                if(j!=i){
                    var lay = this.layers[j];
                    lay.order = --prevOrder;
                }
            }
            break;
        }
    }
    this.layers.sort(compareLayer);
}

/** Move a layer up, ie decrease order value, adjust
 * following layers and resort the layer array.
 * @param {int} gid the layer id
 */
function lg_moveLayerUp(gid){
    var prevOrder = -1;
    var prevIndex = -1;

    for(var i = this.layers.length-1 ; i >= 0; i--){
        var layer = this.layers[i];
        if(layer.visible && layer.gid!=gid){
            prevOrder = layer.order;
            prevIndex = i;
        }
        if( layer.gid == gid && prevIndex!=-1){
            layer.order = prevOrder;
            for(var j = prevIndex; j >= 0; j--){
                if(j!=i){
                    var lay = this.layers[j];
                    lay.order = ++prevOrder;
                }
            }
            break;
        }
    }
    this.layers.sort(compareLayer);
}

/** Create the url to fetch a map image
 * @return the url
 * @type String
 */
function lg_getMapUrl(){
    var resUrl = this.wmsURL;

    resUrl += "request=GetMap";
    resUrl += "&service=WMS";
    resUrl += "&width="+this.parentMap.dimension.width;
    resUrl += "&height="+this.parentMap.dimension.height;
    resUrl += "&version="+this.wmsVer;

    //debug( resUrl );

    if( this.transparent )
        resUrl += "&TRANSPARENT=TRUE";

    if( this.sldName!="undefined"){
       resUrl += "&SLD="+this.sldName;
    }
    else{
        resUrl += "&layers=";

        resLayers = "";
        resStyles = "";
        for( layIdx=0; layIdx < this.layers.length ; layIdx++){
            layer = this.layers[layIdx];
            if( layer.visible ){

                resLayers += layer.getNames()+",";
                resStyles += layer.getStyles()+",";

            }
        }

        // remove last comma char
        resLayers = resLayers.substring(0, resLayers.length-1);
        resStyles = resStyles.substring(0, resStyles.length-1);

        resUrl += resLayers;
        resUrl += "&STYLES="+resStyles;

    }

    resUrl += "&bbox=" + this.parentMap.cBounds.xMin + "," +
                         this.parentMap.cBounds.yMin + "," +
                         this.parentMap.cBounds.xMax + "," +
                         this.parentMap.cBounds.yMax;

    resUrl += "&srs=EPSG:"+this.parentMap.epsg.value;
    resUrl += "&format=" + this.format + "&";
    
    return resUrl;
}

/** Creates the query url for a feature info request
 * @param {int} x the pixel x coordinate
 * @param {int} y the pixel y coordinate
 * @param {Layer} layer the active layer
 * @return a query string
 * @type String
 */
function lg_getFeatureInfoQuery(x,y, layer){
    var qLayers = "";
    if(layer.name != ''){
        qLayers = layer.name;
    }else{
        qLayers = layer.getNames(); 
    }
    var info_format = this.infoFormat;
    
    var map = this.parentMap;

    var sURL = "";	    
    sURL += "request=GetFeatureInfo";
    sURL += "&VERSION=" + this.wmsVer;
    //debug( sURL );
    //sURL += "&INFO_FORMAT="+ info_format;
    //sURL += "&EXCEPTIONS=application/vnd.ogc.se_inimage";
    sURL += "&EXCEPTIONS=application/vnd.ogc.se_xml";
    sURL += "&FEATURE_COUNT=1";
    sURL += "&QUERY_LAYERS=" + qLayers;
    sURL += "&X=" + x;
    sURL += "&Y=" + y;
    sURL += "&WIDTH="+map.dimension.width;
    sURL += "&HEIGHT="+map.dimension.height;
    sURL += "&SRS=EPSG:"+map.epsg.value;
    sURL += "&BBOX=" + map.cBounds.xMin + "," + map.cBounds.yMin + "," + map.cBounds.xMax + "," + map.cBounds.yMax;

    //Geoserver requires params
    sURL += "&FORMAT=" + info_format;
    sURL += "&LAYERS=" + qLayers;
    
    return sURL;
}

// ======================================== Country

/** A country
 * @constructor
 * @param {String} name name of country, eg. Sweden
 * @param {String} code the countrycode, eg. sv
 * @param {Extent} extent the country extent
 */
function Country(name, code, extent){
    /** @type String */
    this.name = name;
    /** @type String */
    this.code = code;
    /** @type Extent */
    this.extent = extent;

    /** @type Array */
    this.groups = new Array();
    /** @type Array */
    this.categories = new Array();

    this.addLayerGroup = co_addLayerGroup;
    this.addCategory = co_addCategory;
}

/** Adds a layergroup to this country
 * @param {LayerGroup} layerGroup the group to add
 */
function co_addLayerGroup(layerGroup){
    this.groups[ this.groups.length ] = layerGroup;
}

/** Adds a feature category to this country
 * @param {Category} category the feature category to add
 */
function co_addCategory(category){
    this.categories[ this.categories.length ] = category;
}

// ======================================== Background

/** Object that holds layer groups for the background
 * @constructor
 */
function Background(){  
  /** @type Array */  
  this.groups = new Array();
  this.addLayerGroup = bg_addLayerGroup;
}

/** Adds a layergroup to the background
 * @param {LayerGroup} layerGroup layer group to be added
 */
function bg_addLayerGroup(layerGroup){
  this.groups[ this.groups.length ] = layerGroup;
}

// ======================================== UserDefined

/** Object that holds the user defined layers
 * @constructor
 */
function UserDefined(){
    /** @type Array */
    this.groups = new Array();
    this.addLayerGroup = ud_addLayerGroup;
    this.deleteLayer = ud_deleteLayer;
    this.deleteLayerGroup = ud_deleteLayerGroup;
}

/** Add a layergroup to user defined layers
 * @param {LayerGroup} layerGroup the group to add
 */
function ud_addLayerGroup(layerGroup){
    layerGroup.isUserDefined = true;
    this.groups[ this.groups.length ] = layerGroup;
}

/** Delete a layer
 * @param {int} layerId id for the layer
 */
function ud_deleteLayer(layerId){
    if(map.isUserDefinedLayer(layerId)){
        var lay = lookupLayer( layerId );
        var layGrp = getLayerGroupForLayer( lay );
        layGrp.layers = deleteLayer(layerId, layGrp.layers );
        g_layers = deleteLayer(layerId, g_layers );        
        map.rebuildUserDefinedDOM();
    }
}

/** Delete a layer group and it's layers
 * @param {int} lgId id for the layer group
 */
function ud_deleteLayerGroup(lgId){
    for(var i = 0; i < this.groups.length; i++){
        var grp = this.groups[i];
        if( grp.gid == lgId ){            

            for(var j = 0; j < grp.layers.length; j++){
                var lay = grp.layers[j];
                g_layers = deleteLayer(lay.gid, g_layers);
            }
            grp.layers = new Array();

            g_layerGroups = deleteLayer(lgId, g_layerGroups);
            this.groups = deleteLayer(lgId, this.groups);            
            map.rebuildUserDefinedDOM();
            break;
        }
    }
}

// ======================================== OgcMap

/**  Create a new ogc map. Upon instantiation this map is added to the
 * global g_maps array.
 * @constructor
 * @param {String} prefix a unique string
 * @param {Dimension} dimension the dimensions of the map
 * @param {Extent} extent the extent for the map
 * @param {Epsg} epsg the Epsg object
 * @param {int} scaleLimit the minimum allowed scale
 * @param {String} lang two letter language code. eg 'sv' or 'en'
 */
function OgcMap(prefix, dimension , extent, epsg, scaleLimit, lang){
   
    var numMaps = g_maps.length;

    /** @type int */
    this.gid = numMaps;	
    /** @type String */
    this.prefix = prefix + "_g"+this.gid+"_";
    /** @type Dimension */
    this.dimension = dimension;
    /** @type Epsg */
    this.epsg = epsg;
    /** @type int */
    this.scaleLimit = scaleLimit;
    /** @type String */
    this.lang = lang;

    /** @type Extent */
    this.mapBounds = extent.copy();
    /** @type Extent */
    this.cBounds = this.mapBounds.copy();
    /** @type Extent */
    this.prevBounds = this.mapBounds.copy();
    /** @type Extent */
    this.prevPrevBounds = this.mapBounds.copy();
    /** @type int */
    this.zoomFactor = 2;
    /** @type boolean */
    this.autoRefresh = true;
    /** @type boolean */
    this.query = true;
    /** @type int */
    this.featureCount = 1;
    /** @type int */
    this.activeLayerId = -1;
    /** @type boolean */
    this.rubberbanding = false;
    /** @type boolean */
    this.refresh = false;
    /** @type boolean */
    this.showingOverview = false;
    /** @type boolean */
    this.progressEnabled = false;
    /** @type int */
    this.timeoutID;

    /** @type Array */
    this.countryExtent = new Array();    
    /** @type Array */
    this.zoomExtent = new Array();    
    /** @type Array */
    this.scaleExtent = new Array();    
    /** @type RubberRectangle */
    this.rubberbander = new RubberRectangle();

    /** @type Menu */
    this.menu = new Menu(epsg,dimension);
    /** @type Overview */
    this.overview;
    /** @type Array */
    this.countries = new Array();
    /** @type Background */
    this.background = new Background();
    /** @type UserDefined */
    this.userDefined = new UserDefined();
        
    this.setOverview = ogcmap_setOverview;
    this.setBackground = ogcmap_setBackground;
    this.setUserDefined = ogcmap_setUserDefined;
    this.addCountry = ogcmap_addCountry;

    this.getHTMLforLayerGroup = ogcmap_getHTMLforLayerGroup;
    this.writeHTML = ogcmap_writeHTML;
    this.writeOverviewHTML = ogcmap_writeOverviewHTML;  
    this.rebuildUserDefinedDOM = ogcmap_rebuildUserDefinedDOM; 
    this.redraw = ogcmap_redraw;
    this.loaded = ogcmap_loaded;
    this.redrawOverviewBox = ogcmap_redrawOverviewBox;
    this.cancelRequest = ogcmap_cancelRequest;    

    this.setMode = ogcmap_setMode;
    this.setExtent = ogcmap_setExtent;
    this.setScaleExtent = ogcmap_setScaleExtent;
    this.fullExtent = ogcmap_fullExtent;
    this.previousExtent = ogcmap_previousExtent;
    this.countryExtent = ogcmap_countryExtent;
    this.addZoomExtent = ogcmap_addZoomExtent;
    this.addScaleExtent = ogcmap_addScaleExtent;
    this.pixelToDD = ogcmap_pixelToDD;
    this.zoomDD = ogcmap_zoomDD;
    this.zoomOut = ogcmap_zoomOut;
    this.pan = ogcmap_pan;    
    this.panDragReset = ogcmap_panDragReset;

    this.buildScaleExtentSelect = ogcmap_buildScaleExtentSelect;
    this.buildZoomExtentSelect = ogcmap_buildZoomExtentSelect;
    this.buildScale = ogcmap_buildScale;
    this.setdefScale = ogcmap_setdefScale;
    this.calculateScale = ogcmap_calculateScale;
    this.calibrateScale = ogcmap_calibrateScale;
    this.dx_from_Scale = ogcmap_dx_from_Scale;
    this.setScaleLimit = ogcmap_setScaleLimit;

    this.toggleRefresh = ogcmap_toggleRefresh;
    this.toggleOverview = ogcmap_toggleOverview;
    this.toggleLayer = ogcmap_toggleLayer;
    this.setLayerVisibility = ogcmap_setLayerVisibility;
    this.setActiveLayer = ogcmap_setActiveLayer;
    this.toggleLayerGroup = ogcmap_toggleLayerGroup;
    this.toggleCategory = ogcmap_toggleCategory;
    this.toggleCategory2 = ogcmap_toggleCategory2;
    this.setVisibilityForLayerInCountries = ogcmap_setVisibilityForLayerInCountries;

    this.isBackgroundLayer = ogcmap_isBackgroundLayer;
    this.isUserDefinedLayer = ogcmap_isUserDefinedLayer;
    this.isLayerInLayerGroup = ogcmap_isLayerInLayerGroup;
    this.getCountryForLayerId = ogcmap_getCountryForLayerId;
    this.getLayerGroup = ogcmap_getLayerGroup;
    this.getLayerGroupByTitle = ogcmap_getLayerGroupByTitle;
    this.findCountry = ogcmap_findCountry;

    this.finish = ogcmap_finish;

    this.featureInfo = ogcmap_featureInfo;
    this.openAddService = ogcmap_openAddService;
    this.openSearch = ogcmap_openSearch;
    this.openHelp = ogcmap_openHelp;
    this.openStyle = ogcmap_openStyle;
    this.openMetadata = ogcmap_openMetadata;
    this.openCapabilities = ogcmap_openCapabilities;  
    this.openEditContext = ogcmap_openEditContext;
	
    g_maps[numMaps] = this;	
}

/** Finish the load, function must be called at end of document.
 * Actions are eg creation of zoom extent options, initial map fetching etc.
 */
function ogcmap_finish(){

    this.buildZoomExtentSelect();
    this.buildScaleExtentSelect();

    this.menu.adjustSize();

    var d = document.getElementById('toolbar');
    d.style.width = this.dimension.width;

    fixCategoryVisibility();
    fixLayerOrdering();

    this.toggleRefresh();
    this.buildScale();
    this.progressEnabled = false;
    this.redraw();     
    this.progressEnabled = true;
}

/** Set map's scaleLimit attribute to tag.value
 * @param {Object} tag the html element holding new value
 */
function ogcmap_setScaleLimit(tag){
    this.scaleLimit = tag.value;
}

/** Toggles the visibility of a layer, redraws the map
 * if the auto refresh is enabled. If variable this.menu.selectAllCountries 
 * is true, the layers in all countries with same name will be toggled.
 * @param {int} gid the layer id
 */
function ogcmap_toggleLayer(gid){
    layer = lookupLayer(gid);
    
    this.setLayerVisibility(gid, !layer.visible );    
}

/** Set visibility for a layer, redraws the map if auto refresh is enabled.
 * If variable this.menu.selectAllCountries 
 * is true, the visibility for layers in all countries with same name will be set
 * @param {int} gid id for layer to set visibility
 * @param {boolean} enable true if layer should be visible, false otherwise
 */
function ogcmap_setLayerVisibility(gid, enable ){
    layer = lookupLayer(gid);
    
    layer.visible = enable;

    if( this.menu.selectAllCountries ){
        this.setVisibilityForLayerInCountries( layer, layer.visible );
    }
    
    if( this.refresh )
        this.redraw();
}

/** Set visibility for layers in all countries that has some name
 * in common with specified layerObj.
 * @param {Layer} layerObj
 * @param {boolean} visible true if layers should be visible, false otherwise
 */
function ogcmap_setVisibilityForLayerInCountries( layerObj, visible ){
  for(var i = 0; i < this.countries.length; i++){
      var country = this.countries[i];

      for(var j = 0; j < country.groups.length; j++){
            var group = country.groups[j];
            for(var k = 0; k < group.layers.length; k++){
                var layer = group.layers[k]; 
           
                if( layer.hasName( layerObj ) )
                    layer.visible = visible;
            }
      }
  }
}

/** Toggles the visibility of all layers in a layergroup, redraws
 * the map if the auto refresh is enabled
 * @param {int} groupId the layergroup id
 */
function ogcmap_toggleLayerGroup(groupId){
  var inputTags = document.getElementsByTagName('input');

  var layerGroup = lookupLayerGroup(groupId);
  layerGroup.allChecked = !layerGroup.allChecked;

  for(var i=0; i < inputTags.length; i++){
     var tag = inputTags[i];
     if( tag.id.indexOf("lay:"+groupId) == 0){        
        var parts = tag.id.split(':');
        var layer = lookupLayer( parts[2] );
        layer.visible = layerGroup.allChecked; 
        tag.checked= layerGroup.allChecked; 
     }
  }
  if( this.refresh )
      this.redraw();   
}

/** Set visibility for layers in a category. If category flag allChecked is
 * true, layers visibility and allChecked is set to false.
 * @param {int} categoryId id for the category
 */
function ogcmap_toggleCategory(categoryId){
    var category = g_categories[categoryId];
    this.toggleCategory2(categoryId, !category.allChecked);
}

/** Set visibility for layers in a category. Category allChecked flag
 * will be set, also for all countries (matching categories on title) 
 * if menu.selectAllCountries is true. If auto refresh is on, the map is redrawn.
 * @param {int} categoryId id for the category
 * @param {boolean} enable true if layers should be visible, false otherwise
 */
function ogcmap_toggleCategory2(categoryId, enable){
  var inputTags = document.getElementsByTagName('input');
 
  var category = g_categories[categoryId];
  category.allChecked = enable;

  // disable refreshing and restore later
  var tmpRefresh = this.refresh;
  this.refresh = false;

  for(var i=0; i < inputTags.length; i++){
     var tag = inputTags[i];
     if( tag.id.indexOf("cat:"+categoryId) == 0){        
        var parts = tag.id.split(':');
        var layerGID = parts[2];
 
        tag.checked= enable;

        this.setLayerVisibility( layerGID, enable ); 
     }
  }

  if( this.menu.selectAllCountries ){
        for(var i=0; i < this.countries.length; i++){
            var country = this.countries[i];
            for(var j=0; j < country.categories.length; j++){
                var cat = country.categories[j];
                if( cat.title == category.title ){
                    cat.allChecked = enable;
                }
            }
        }
  }

  this.refresh = tmpRefresh;

  if( this.refresh )
      this.redraw();   
}

/** Toggles the auto refresh and updates the checkbox found at tag
 * id <code>refresh</code>.
 */
function ogcmap_toggleRefresh(){
    this.refresh = !this.refresh;
    var ckb = document.getElementById('refresh');
    ckb.checked = this.refresh;
}

/** Sets the active layer and updates the innerHTML for
 * tag id <code>activeLayer</code>.
 * @param {int} layerId the layer gid
 */
function ogcmap_setActiveLayer(layerId){
    this.activeLayerId = layerId;

    var layer = lookupLayer(layerId);
    var active = document.getElementById('activeLayer');

    var prefix = '';
    if( this.isBackgroundLayer(layerId) ){
        prefix = i18n('js.background') + ' - ';
    }
    else if( this.isUserDefinedLayer(layerId) ){
        prefix = i18n('js.overlay')+' - ';
    }
    else{
        var country = this.getCountryForLayerId(layerId);
        prefix = country.name + ' - ';
    }

    active.innerHTML = prefix+layer.displayName;
}

/** Check if layer is a background layer
 * @param {int} layerId the layer gid
 * @return true if layer with specified gid is a background
 *         layer, false otherwise
 * @type boolean
 */
function ogcmap_isBackgroundLayer(layerId){
    return this.isLayerInLayerGroup(layerId, this.background.groups);
}

/** Check if layer is a user defined layer
 * @param {int} layerId the layer gid
 * @return true if layer with specified gid is a user defined
 *         layer, false otherwise
 * @type boolean
 */
function ogcmap_isUserDefinedLayer(layerId){
    return this.isLayerInLayerGroup( layerId, this.userDefined.groups );
}

/** Check if layer is in a layergroup.
 * @param {int} layerId the layer gid
 * @param {Array} layerGroups an array of LayerGroup objects
 * @return true if layer is in any of specified layer groups, false otherwise
 * @type boolean
 */
function ogcmap_isLayerInLayerGroup(layerId, layerGroups){
    for( var i = 0; i < layerGroups.length; i++){
        var layerGroup = layerGroups[i];
        for( var j = 0; j< layerGroup.layers.length; j++){
            var layer = layerGroup.layers[j];
            if( layer.gid == layerId){
                return true;
            }
        }
    }
    return false;
}

/** Find layer group in user defined layers with a specific title
 * @param {String} title the title to look for
 * @return the LayerGroup with matching title, false otherwise
 * @type mixed
 */
function ogcmap_getLayerGroupByTitle(title){
   var layerGroups = this.userDefined.groups;
    for( var i = 0; i < layerGroups.length; i++){
        var layerGroup = layerGroups[i];
        if( layerGroup.title == title){
            debug("Layergroup found");
            return layerGroup;
        }
     }
   
    return false;
}


/** Get the country that owns specified layer
 * @param {int} layerId the layer gid
 * @return the Country that owns the layer, false if none was found
 * @type mixed
 */
function ogcmap_getCountryForLayerId(layerId){

    for( var i = 0 ; i < this.countries.length; i++){
        var country = this.countries[i];
        if( this.isLayerInLayerGroup( layerId, country.groups ) ){
            return country;
        }
    }
    return false;
}

/** Set the overview for this map
 * @param {Overview} overview the overview
 */
function ogcmap_setOverview(overview){
    this.overview = overview;
}

/** Show or hide the overview
 */
function ogcmap_toggleOverview(){
   var ov = document.getElementById("overview");   
   toggleVisibility( ov ); 
   
   var ovb = document.getElementById('ovbox');
   
   toggleVisibility( ovb );
   
   this.showingOverview = !this.showingOverview;
   
   if(this.showingOverview)
     this.redrawOverviewBox();
}

/** Draw the current extent in the overview map
 */
function ogcmap_redrawOverviewBox(){
    var ovbox = document.getElementById('ovbox');
    
    var ulpix = this.overview.DDtoPixel(map.cBounds.xMin, map.cBounds.yMax );         
    var lrpix = this.overview.DDtoPixel(map.cBounds.xMax, map.cBounds.yMin );
   	
    ovbox.style.left   = ulpix.x;
    ovbox.style.top    = ulpix.y;
    ovbox.style.width  = lrpix.x - ulpix.x;
    ovbox.style.height = lrpix.y - ulpix.y;
}

/** Set the map extent to a zoom extent (using <code>select</code> tag)
 * and redraw (using function fullExtent() )
 * @param selectTag the select tag containing zoom extents
 */
function ogcmap_setExtent(selectTag){
    for(i=0 ; i< this.zoomExtent.length ; i++){
        if(selectTag.options[selectTag.selectedIndex].text == this.zoomExtent[i].region ){
            this.mapBounds = this.zoomExtent[i].extent.copy();
	    break;
        }
    }
    this.fullExtent();
}

/** Set the map extent to a predefined scale (using <code>select</code> tag)
 * and redraw (using function redraw() )
 * @param selectTag the select tag containing scale denominator
 */
function ogcmap_setScaleExtent(selectTag){    
    for(i=0 ; i< this.scaleExtent.length ; i++){
        if(selectTag.options[selectTag.selectedIndex].text == this.scaleExtent[i].name ){         
         this.setdefScale(this.scaleExtent[i].scale);       
	      break;
        }
    }
    this.redraw();    
}

/** Set the current map extent to the full extent and redraw the map.
    Note should implement code to reset the scale option selection to blank
    if there is a change in setExtent i.e fullextent()
 */
function ogcmap_fullExtent(){    
    this.prevBounds = this.cBounds.copy();
    this.cBounds = this.mapBounds.copy();
    	
    this.redraw();
    
}

/** Set the current extent to the previous extent and redraw the map.
 */
function ogcmap_previousExtent(){
    var tmpBounds = this.cBounds.copy();
    this.cBounds = this.prevBounds.copy();
    this.prevBounds = tmpBounds.copy();
    this.redraw();
}

/** Set the current extent to the country extent and redraw the map.
 * @param {String} countryCode the code for the country whose extent will be used
 */
function ogcmap_countryExtent(countryCode){
    var country = this.findCountry(countryCode);
    if( country ){
            this.prevBounds = this.cBounds.copy();
 	    this.cBounds = country.extent.copy();
 	    this.redraw();
    }
    else{
        alert( i18n('js.countrynotfound') + ': ' + countryCode);
    }
}

/** Get the country specified by code
 * @param {String} countryCode code for the desired country
 * @return the Country with specified code, false if none found
 * @type mixed
 */
function ogcmap_findCountry(countryCode){
    for(var i=0;i<this.countries.length;i++){
        var country = this.countries[i];
  	    if( country.code == countryCode ){
            return country;
  	    }
    }
    return false;
}

/** Write DOM for zoom extent select tag (id <code>zoomExtentSelect</code>)
 */
function ogcmap_buildZoomExtentSelect(){
    var sel = document.getElementById('zoomExtentSelect');
    for( i = 0; i< this.zoomExtent.length;i++){
        var ze = this.zoomExtent[i];
        var opt = new Option(ze.region, ze.region);
        sel.options[ sel.length ] = opt;
    }
}

/** Add a zoomextent to this map.
 * @param {ZoomExtent} zoomExtent the ZoomExtent to add
 */
function ogcmap_addZoomExtent(zoomExtent){
    this.zoomExtent[ this.zoomExtent.length ] = zoomExtent;
}

/** Write DOM for scale extent select tag (id <code>scaleExtentSelect</code>)
 */
function ogcmap_buildScaleExtentSelect(){
    var sel = document.getElementById('scaleExtentSelect');
    for( i = 0; i< this.scaleExtent.length;i++){
        var sc = this.scaleExtent[i];
        var opt = new Option(sc.name, sc.scale);
        sel.options[ sel.length ] = opt;
    }
}

/** Add a scaleextent to this map.
 * @param {ScaleExtent} scaleExtent the ScaleExtent to add
 */
function ogcmap_addScaleExtent(scaleExtent){
    this.scaleExtent[ this.scaleExtent.length ] = scaleExtent;
}

/** Set a predefiend scale
    and zoom at the current center coordinate
 * @return none
 */

function ogcmap_setdefScale(scale){ 
    if(this.epsg.unit == 'meter'){
        //get the coordinate of the centre pixel
        var ax = this.cBounds.xMin + ((this.cBounds.xMax-this.cBounds.xMin)/2);
        var ay = this.cBounds.yMin + ((this.cBounds.yMax-this.cBounds.yMin)/2);
        //half width/height of the extent at the new scale
        var dx = scale*this.dimension.width*0.00028;
        var dy = scale*this.dimension.height*0.00028;
    }
    else{
        //get the coordinate of the centre pixel
	var ax = this.cBounds.xMin + (this.cBounds.getDX()/2);
	var ay = this.cBounds.yMin + (this.cBounds.getDY()/2);
	 
        var dx = this.dx_from_Scale(scale);
	var dy = (dx*this.cBounds.getDY()) / this.cBounds.getDX();
        
    }
    this.prevBounds = this.cBounds.copy();
    this.cBounds.xMin = ax - dx/2;
    this.cBounds.xMax = ax + dx/2;
    this.cBounds.yMin = ay - dy/2;
    this.cBounds.yMax = ay + dy/2;	 
}

/** Calculates the current scale
 * @return the current scale
 * @type int
 */
function ogcmap_calculateScale(){    
    var c;

    if(this.epsg.unit == 'meter'){
        var b = (this.cBounds.xMax-this.cBounds.xMin);
        c = Math.round( (b/this.dimension.width)/0.00028 );
    }
    else{
        var pi= Math.atan(1)*4;
        var b = (this.cBounds.xMax-this.cBounds.xMin) * (6378137 * 2 * pi)/360.0;
        c = Math.round( (b/this.dimension.width)/0.00028);
    }

    return c;
}

/** Calculate the scale from a given bbox width.
    @param {dx} bbox width
    @type double
    @return the scale
    @type double     
 */
function ogcmap_calibrateScale(dx){
    var c;

    if(this.epsg.unit == 'meter'){
        c = Math.round( (dx/this.dimension.width)/0.00028 );
    }
    else{
       var pi= Math.atan(1)*4;
       var b = (dx) * (6378137 * 2 * pi)/360.0;
       c = Math.round( (b/this.dimension.width)/0.00028);
    }    
    return c;
}

/** Calculate the bbox width from a given scale.
    @param {scale} the scale 
    @type double
    @return the bbox width
    @type double     
 */
function ogcmap_dx_from_Scale(scale){ 
    var dx;

    if(this.epsg.unit == 'meter'){
        dx = scale*0.00028*(this.dimension.width);
    }
    else{
        var pi= Math.atan(1)*4;
        var a = (6378137 * 2 * pi)/360.0;

        var b = scale*0.00028*(this.dimension.width);
        dx = (b/a);
    }
    return dx;
}

/** Determine the current mapscale and update innerHTML for element
 * with id <code>scale</code>
 */
function ogcmap_buildScale(){  	
    var scale = this.calculateScale();
    var scaleHook = document.getElementById('scale');
    scaleHook.innerHTML = "1:" + formatNumber(scale);
}

/** Set the maptool mode
 * <ol><li>Zoom in</li><li>Zoom out</li><li>Pan</li><li>Feature info</li></ol>
 * @param {int} mode the mode
 */
function ogcmap_setMode(mode){

    if(mode==3){
        document.getElementById('dmapimage').style.cursor='url(images/hand_open.cur), pointer';
    }
    else{
        document.getElementById('dmapimage').style.cursor='crosshair';
    }
    this.mode = mode;
}

/** Set the UserDefined object, both for this map and the Menu object
 * @param {UserDefined} userDefiend the UserDefined object
 */
function ogcmap_setUserDefined(userDefined){
    this.userDefined = userDefined;
    this.menu.setUserDefined(userDefined);
}

/** Set the Background object, both for this map and the Menu object
 * @param {Background} background the Background object
 */
function ogcmap_setBackground(background){    
    this.background = background;
	this.menu.setBackground(background);
}

/** Add a country, update the country array in the Menu object
 * @param {Country} country the Country to add
 */
function ogcmap_addCountry(country){
    this.countries[ this.countries.length ] = country;
    this.menu.setCountries(this.countries);
}

/** Zooms in on the specified pixel coordinate
 * @param {int} xp pixel x-coordinate
 * @param {int} yp pixel y-coordinate
 */
function ogcmap_zoom(xp,yp){
	 
    this.prevBounds = this.cBounds.copy();

    var dx = this.cBounds.getDX() / this.zoomFactor;    
    var dy = this.cBounds.getDY() / this.zoomFactor;    

    var dd = this.pixelToDD(xp,yp);

    this.cBounds.xMin = dd[0]-(dx/2.0);
    this.cBounds.xMax = dd[0]+(dx/2.0);
    this.cBounds.yMin = dd[1]-(dy/2.0);
    this.cBounds.yMax = dd[1]+(dy/2.0);

    this.redraw();
}

/** Does the opposite of zoom()
 * @param {int} x longitude
 * @param {int} y latitude
 */
function ogcmap_zoomOut(x,y){	 	
    this.prevBounds = this.cBounds.copy();
   
    var dx = this.cBounds.getDX() * this.zoomFactor;
    var dy = this.cBounds.getDY() * this.zoomFactor;
    
    this.cBounds.xMin = x-(dx/2.0);
    this.cBounds.xMax = x+(dx/2.0);
    this.cBounds.yMin = y-(dy/2.0);
    this.cBounds.yMax = y+(dy/2.0);
    
    if(this.epsg.unit != 'meter'){
        if(this.cBounds.xMin < -180 || this.cBounds.xMax > 180 || this.cBounds.yMin < -90 || this.cBounds.yMax > 90 ){
	    // limit
            this.cBounds.xMin = -180;
            this.cBounds.yMin = -90;
            this.cBounds.xMax =  180;
            this.cBounds.yMax =  90;	 
        }
    }
    this.redraw();
}

/** Zooms in on the specified map coordinate
 * @param x longitude
 * @param y latitude
 */
function ogcmap_zoomDD(x,y){

    this.prevBounds = this.cBounds.copy();
    
    var dx = this.cBounds.getDX() / this.zoomFactor;    
    var dy = this.cBounds.getDY() / this.zoomFactor;

    this.cBounds.xMin = x-(dx/2.0);
    this.cBounds.xMax = x+(dx/2.0);
    this.cBounds.yMin = y-(dy/2.0);
    this.cBounds.yMax = y+(dy/2.0);

    this.redraw();
}

/** Reset map after pan, ie move the map div to original place
 */
function ogcmap_panDragReset(){
    document.getElementById('dmapimage').style.cursor='url(images/hand_open.cur), pointer';
    var drag = document.getElementById('map_g0_');
    drag.style.clip = 'rect(0,'+this.dimension.width+','+this.dimension.height+',0)';
    drag.style.left = '0';
    drag.style.top = '0';    
}

/** Center map around specified pixel coordinates
 * @param {int} xp pixel x-coordinate
 * @param {int} yp pixel y-coordinate
 */
function ogcmap_pan(xp,yp){

    this.prevBounds = this.cBounds.copy();

    var dx = this.cBounds.getDX();
    var dy = this.cBounds.getDY();

    this.cBounds.xMin = xp-(dx/2.0);
    this.cBounds.xMax = xp+(dx/2.0);
    this.cBounds.yMin = yp-(dy/2.0);
    this.cBounds.yMax = yp+(dy/2.0);

    this.redraw();
}

/** Converts pixel to decimal degree coordinates
 * @param xp x coordinate
 * @param yp y coordinate
 * @param prec
 * @return array[0..1], 0 = longitude, 1 = latitude
 * @type Array
 */
function ogcmap_pixelToDD(xp,yp, prec){
    var aDD = new Array(0,0);
    var dx = this.cBounds.getDX();
    var dy = this.cBounds.getDY();
    
    // TODO use object instead of array
    if(prec){
        aDD[0] = i18n('js.lon')+" = "+ Math.round(prec*( this.cBounds.xMin + (dx*xp)/this.dimension.width))/prec;
        aDD[1] = i18n('js.lat')+" = "+ Math.round(prec*( this.cBounds.yMax - (dy*yp)/this.dimension.height))/prec;
    }
    else{
        aDD[0] = this.cBounds.xMin + (dx*xp)/this.dimension.width;
        aDD[1] = this.cBounds.yMax - (dy*yp)/this.dimension.height;
    }

    return aDD;
}

/** Write the html for the overview to document.
 */
function ogcmap_writeOverviewHTML(){
   var width = this.overview.dimension.width;
   var height = this.overview.dimension.height;
   
   var swh = "width='" + width + "px' height='" +width + "px'";
   var s="";
   
    s += "<div style='width:" + width + "px; height:" + height + "px; ' id='" + this.prefix + "_ov'>\n";
	
    for( i = 0; i < this.overview.groups.length ; i++){       
	   var layerGroup = this.overview.groups[i];
	   s += this.getHTMLforLayerGroup( layerGroup, width, height );
    }		
    
    s += "<div id='ovbox' name='ovbox' style='visibility:hidden; border: 1px solid #FF0000;'>";	
    s += "<div style='visibility:hidden'></div>";
    s += "</div>";
    
    s += '</div>'; 
    
    document.write(s);
}

/** Rebuild the HTML DOM for user defined layers (overlay layers). The innerHTML property
 * of element 'userDefinedLayers' is set.
 */
function ogcmap_rebuildUserDefinedDOM(){
    var width = this.dimension.width;
    var height = this.dimension.height;
   
    var s = '';
    for( i = 0; i < this.userDefined.groups.length ; i++){
 	   var layerGroup = this.userDefined.groups[i];           
 	   s += this.getHTMLforLayerGroup( layerGroup, width, height );
    }
    var usrDefDiv = document.getElementById('userDefinedLayers');
    usrDefDiv.innerHTML = s;
}

/** Write the html for the map to document.
 */
function ogcmap_writeHTML(){
    var width = this.dimension.width;
    var height = this.dimension.height;

    var swh = "width='" + width + "px' height='" +width + "px'";
    var s="";
    s += '<div style="width:' + width + 'px; height:' + height + 'px; ">';
    s += '<div style="position: absolute; top: 0; left: 0; width:' + width + 'px; height:' + height + 'px; " id="' + this.prefix + '">\n';

    // background layers
    for( i = 0; i < this.background.groups.length ; i++){
        var layerGroup = this.background.groups[i];
	s += this.getHTMLforLayerGroup( layerGroup, width, height );
    }

    // country layers
    for( var c = 0; c < this.countries.length ; c++){
        var country = this.countries[c];
 	   for( i = 0; i < country.groups.length ; i++){
 	       var layerGroup = country.groups[i];
 	       s += this.getHTMLforLayerGroup( layerGroup, width, height ); 	      
        }
    }

    s += '<div id="userDefinedLayers">';
    // user defined layers
    for( i = 0; i < this.userDefined.groups.length ; i++){
 	   var layerGroup = this.userDefined.groups[i];
 	   s += this.getHTMLforLayerGroup( layerGroup, width, height ); 	   
    }

    s += '</div>';

    s+="<div style='width:" + width + "px; height:" + height + "px; margin:0; padding:0; position:absolute; z-index:1000;'>";

    s+='<div id="dmap">';
    s+="<input id='dmapimage' type='image' src='"+g_blank+"' height='100%' width='100%'>\n";
    s+="<div id='box' style='visibility:hidden;border: 1px solid #FF0000;'>";
    s+="<div style='visibility:hidden;'></div>";
    s+="</div>";
    s+="</div>";

    s += "</div>";

    var progBoxWidth = 200;
    var posLeft = ((width/2)-(progBoxWidth/2));
    var posTop = height/2;

    s += '<div id="progress" style="width:' + width + 'px; height:' + height + 'px;">';
    s += '<div id="progressBox" style="top: '+posTop+'px; width: '+progBoxWidth+'px; left: '+posLeft+'px">';
    s += '<span id="progressHook">'+i18n('js.init')+'</span>';
    s += '</div></div>';

    s += '<div id="pan_cover" style="width:' + width + 'px; height:' + height + 'px;"></div>';

    s += "</div>";
    
    s+= '</div>' 
    document.write(s); 
}

/** Create the html representing a layergroup.
 * @param {LayerGroup} layerGroup the LayerGroup
 * @param {int} width the width of the image
 * @param {int} height the height of the image
 * @return generated html
 * @type String
 */
function ogcmap_getHTMLforLayerGroup(layerGroup, width, height){
    var id = this.prefix+layerGroup.gid;
   
    var s='';
    s += '<div name="'+id+'" id="'+id+'" style="width:' + width + 'px; height:' + height + 'px; margin:0; padding:0; position:absolute;z-index:'+layerGroup.zIndex+';">';
    s += '<img onload="map.loaded('+layerGroup.gid+')" name="' +id+ '" src="'+g_blank+'" width="'+width+'" height="'+height+'">';    
    s += '</div>\n';
    return s;
}

/** Redraw the map. First call will replace dummy blank images with real
 * fetched map images.
 */
function ogcmap_redraw(){
    var scale = map.calculateScale();
    
    if(scale < map.scaleLimit){
         alert( i18n('js.scalelimit') );
         this.cBounds = this.prevBounds.copy();
         this.prevBounds = this.prevPrevBounds.copy();
         hide('pan_cover'); 
         map.buildScale();         
         return;
    }

    var showProgress = false;
    var imgTags = document.getElementsByTagName('img');

    for( var i = 0; i< imgTags.length; i++){
        var imgTag = imgTags[i];

        // don't handle images without maps
        if(!imgTag.name)
            continue;
        else  if(imgTag.name.indexOf('map') == -1)
            continue;

        var parts = imgTag.name.split('_');
        var layerGroupID = parts[2];
        var layerGroup = lookupLayerGroup(layerGroupID);
    
        if( layerGroup.hasVisibleLayer() ){ 
            layerGroup.loading = true;
            showProgress = true;            
            imgTag.src = layerGroup.getMapUrl();
        }
        else{                        
            imgTag.src = g_blank;
        }
    }
    
    if( showProgress )
        if( map.progressEnabled ){
            this.timeoutID = null;
            this.timeoutID = setTimeout('progress()', 200);
        }

    if( this.showingOverview ){
        this.redrawOverviewBox();
    }
    
    this.buildScale();
}

/** Cancel a redraw, ie hide the progress indicator and set
 * layer groups loading flag to false.
 */
function ogcmap_cancelRequest(){
    hide('progress');

    for( var i = 0; i < g_layerGroups.length; i++){
        layerGroup = g_layerGroups[i]; 
        layerGroup.loading = false;
    }        
}
	
/** Callback function that is called when a image (layer group) is loaded.
 * The flag loading is set to false.
 */
function ogcmap_loaded(layerGroupId){

    layerGroup = lookupLayerGroup(layerGroupId);
    layerGroup.loading = false;
    //debug('Loaded '+layerGroup.title);
}

/** Function to handle the progress of loading maps. It shows how many
 * downloaded maps in the progress indicator. This function calls it self
 * at an interval of 1 sec and when all maps are loaded (or cancelled)  
 */
function progress(){
    var notLoaded = 0;
    var totalToLoad = 0;
    var title = '';

    for( var i = 0; i < g_layerGroups.length; i++){
        var layerGroup = g_layerGroups[i]; 
        if( layerGroup.hasVisibleLayer() )
            totalToLoad++;

        if( layerGroup.loading ){
            notLoaded++;
            title = layerGroup.title;
        }
    }  

    if( notLoaded > 0 ){  
             
        var progress = document.getElementById('progressHook');
        var s = i18n('js.dwn.dwn') + ' ' + (totalToLoad-notLoaded) + ' '+i18n('js.dwn.of') +' ' + totalToLoad + '<br>';
        s += i18n('js.dwn.wait')+ ' ' + title+'<br><a href="javascript: map.cancelRequest();">'+i18n('js.dwn.cancel')+'</a>';
        progress.innerHTML = s;
        show('progress');
        setTimeout('progress()',1000);
    }
    else{
        hide('progress'); 
        hide('pan_cover');
    }
    
}

/**
 * Open the Context frame
 */
function ogcmap_openEditContext(){
    var w = window.open("jsp/editContext.jsp","ManageContextWin","height=215,width=350,resizable=yes,status=yes,toolbar=no,menubar=yes,location=no,scrollbars=yes",false);
    w.focus();
}

/**
 * Open the search frame
 */
function ogcmap_openSearch(){
    var w = window.open("jsp/search.jsp","SearchWin","height=280,width=420,resizable=yes,status=yes,toolbar=no,menubar=yes,location=no,scrollbars=yes",false);
    w.focus();
}


/**
 * Open the style frame
 * PSY 2006-03-02 added encodeURIComponent() to the layer.title in the url sent to wmsRequest
 * this was needed for characters like едц. 
 */
function ogcmap_openStyle(groupId,layerId){
    var layerGroup = lookupLayerGroup( groupId );
    var layer = lookupLayer( layerId );

    var sURL = layerGroup.wmsURL + '&REQUEST=GetCapabilities&EXCEPTIONS=application/vnd.ogc.xml';
    var request = sURL.replace(/&/gi,",,");
    var url ="wmsRequest?REQUEST="+request+"&tool=2&groupId="+groupId+"&layer="+encodeURIComponent(layer.title)+"&layerId="+layerId+"&display="+encodeURIComponent(layer.displayName);
    //debug( "openStyle "+ url);
    var w = window.open(url,"StyleWin","height=200,width=500,resizable=yes,status=yes,toolbar=no,menubar=no,location=no,scrollbars=yes",false);
    w.focus();
}

/**
 * Open the help frame
 * PSY 2006-03-14
 */
function ogcmap_openHelp(){
    var url="documents/Hands-on-training-V1.pdf";
    var w = window.open(url,"HelpWindow","height=620,width=800,resizable=yes,status=no,toolbar=no,menubar=yes,location=no,scrollbars=yes",false);
    w.focus();		
}
/**
 * Open the metadata frame
 */
function ogcmap_openMetadata(){
    
    if( this.activeLayerId < 0){
        alert("No active layer");
        return;
    }
		
    var activeLayer = lookupLayer( this.activeLayerId );
    var activeLayerGroup = getLayerGroupForLayer( activeLayer );

    var sURL = activeLayerGroup.wmsURL + '&REQUEST=GetCapabilities&EXCEPTIONS=application/vnd.ogc.xml';
    var request = sURL.replace(/&/gi,",,");
    var layerName = activeLayer.getNames().toLowerCase();
    var type = 'xml';

    var url = 'metaview?host=' + request + '&type='+type+'&layer=' + layerName + '&content=default';
    var w = window.open(url,"MetadataWindow","height=620,width=800,resizable=yes,status=no,toolbar=no,menubar=yes,location=no,scrollbars=yes",false);
    w.focus();		
	
}

/**
 * Open the capability frame
 */
function ogcmap_openCapabilities(groupId){
 
    var layerGroup = lookupLayerGroup( groupId );
	
    var sURL = layerGroup.wmsURL + '&SERVICE=WMS&REQUEST=GetCapabilities&EXCEPTIONS=application/vnd.ogc.xml';
    var request = sURL.replace(/&/gi,",,");
    var url ="wmsRequest?REQUEST="+request+"&tool=1";

    var w = window.open(url,"CapabilitiesWindow","height=600,width=500,resizable=yes,status=yes,toolbar=no,menubar=no,location=no,scrollbars=yes",false);
	w.focus();
}


/** Open the addService frame
 */
function ogcmap_openAddService(){  
    var w = window.open("jsp/addService.jsp","addServiceWindow","height=450,width=500,resizable=yes,status=yes,toolbar=no,menubar=no,location=no,scrollbars=yes",false);	
}

/**
 * Open the feature info frame
 * @param x the x coordinate of user click
 * @param y the y coordinate of user click
 */
function ogcmap_featureInfo(x,y){

   if( this.activeLayerId < 0 ){
     alert( i18n('js.noactive') );
     return;
   }
   
   var activeLayer = lookupLayer( this.activeLayerId );
   var layerGroup = getLayerGroupForLayer( activeLayer );
   
   var sURL = layerGroup.wmsURL + '&' + layerGroup.getFeatureInfoQuery(x,y,activeLayer);
   var request = sURL.replace(/&/gi,",,");
   var url = "wmsRequest?REQUEST="+request+"&tool=0";
   //debug("featureInfo request "+url);
   var w = window.open(url,"FeatureInfoWindow","height=400,width=300,resizable=yes,status=no,toolbar=no,menubar=no,location=no,scrollbars=yes",false);
   w.focus();
}

// =========================================
function debugLayers(){
  var s='';
  for(var i = 0; i<g_layers.length;i++){
    var l = g_layers[i];
    s += l.gid+': '+l.displayName+'\n';
  }
  alert(s);
}


function debugWin(){
  var includeLayers = false;

  var s = 'Background\n';
  for(var i = 0; i<map.background.groups.length;i++){
    var group =map.background.groups[i];
    s += '.Group: '+group.title+' loading:' +group.loading +' hasVisible: '+group.hasVisibleLayer()+'\n';
    for(var j = 0; j<group.layers.length;j++){
      var l = group.layers[j];
      if(includeLayers) s += '..'+l.gid+': '+l.displayName+'\n';
    }

  }
    s += 'Countries\n';
  for(var k = 0; k<map.countries.length;k++){
   var  country = map.countries[k];
     s+='.'+country.name+'\n';
  for(var i = 0; i<country.groups.length;i++){
    var group =country.groups[i];
    s += '..Group: '+group.title+' loading: '+group.loading + ' hasVisible: '+group.hasVisibleLayer()+'\n';
    for(var j = 0; j<group.layers.length;j++){
      var l = group.layers[j];
      if(includeLayers)s += '...'+l.gid+': '+l.displayName+'\n';
    }

  }
    }
  s += 'User Defined\n';
  for(var i = 0; i<map.userDefined.groups.length;i++){
    var group =map.userDefined.groups[i];
    s += '.Group: '+group.title+' loading: '+group.loading+' hasVisible: '+group.hasVisibleLayer()+'\n';
    for(var j = 0; j<group.layers.length;j++){
      var l = group.layers[j];
      if(includeLayers)s += '..'+l.gid+': '+l.displayName+'\n';
    }

  }
  alert(s);
}

/**
 * @Get layergroup.
 * @param layerId the layer gid
 * @param layerGroups an array of LayerGroup objects
 * @return the group if layer is in any of specified layer groups, false otherwise
 * @type boolean
 */

function ogcmap_getLayerGroup(layerId, layerGroups){
    for( var i = 0; i < layerGroups.length; i++){
        var layerGroup = layerGroups[i];
        for( var j = 0; j< layerGroup.layers.length; j++){
            var layer = layerGroup.layers[j];
            if( layer.gid == layerId){
                return layerGroup;
            }
        }
    }
    return false;
}

