/*
***** BEGIN LICENSE BLOCK *****
Version: MPL 1.1

The contents of this file are subject to the Mozilla Public License Version 
1.1 (the "License"); you may not use this file except in compliance with 
the License. You may obtain a copy of the License at 
http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.

The Original Code is Vis Stedet javascript core.

The Initial Developer of the Original Code is National Survey and Cadastre of Denmark.
Portions created by the Initial Developer are Copyright (C) 2008
the Initial Developer. All Rights Reserved.

Contributor(s):
  Jes Wulfsberg Nielsen <jes@northtech.dk>
  Jon Gregor Hemmingsen <jon@northtech.dk>
  Julian Hollingbery <julho@kms.dk>
  Per Reippuert Kristensen <ppk@kms.dk>
  Søren Riff Alexandersen <sra@kms.dk>

***** END LICENSE BLOCK *****
*/

/*
 Class: VisStedet
 VisStedet is the scope for all objects provided by the Vis Stedet project. Many of the objects in here are
 extensions and/or specializations of OpenLayers or Scriptaculous objects.
 The core javascript file also includes utility functions, typically to initialize the standard components with reasonable defaults,
 but these are strictly speaking not a part of the Vis Stedet scope.
 */
VisStedet = {};

if(typeof Autocompleter!='undefined')
{
/*
 Class: VisStedet.JSONDropdown

 Extension of the Scriptaculous Autocompleter class, adding GeoEvent listener functionality hooked to the JSON objects.
 This is both a fully functional standard component (AJAX dropdown autocompletion of a search returning JSON geoevent data)
 and a practical example of extending and customizing more generic components to provide a specific service within the GeoEvent model.
 See the get...Dropdown functions for examples on how to wire this component to specific JSON services.
 */
VisStedet.JSONDropdown = Class.create();
Object.extend(Object.extend(VisStedet.JSONDropdown.prototype, Autocompleter.Base.prototype),
{
    listeners: [],
    foundElements: null,
    url: null,
    paramName: null,
    
    /*
     Constructor: VisStedet.JSONDropdown
     Creates an object to handle the async communication to a generic JSON search service, keyed to the HTML elements to show the results.
     
     Parameters:
     url - {String} The URL to the JSON service. This can potentially include URL parameters, for example switches for the specific service.
     paramName - {String} The URL parameter name used when sending the input field to the JSON service.
     element - {String} The HTML element name this object will use when showing the results.
     update - {String} The HTML element name of the DIV tag which will be filled out with the selectable options. By convention in Vis Stedet, this should be the same as element, postfixed with '_choices'.
     options - {Object} An optional object with additional configuration properties. This will typically be the number of characters required before the JSON communication triggers, and the HTML element to function as the "working" indicator.
     */
    initialize: function(url, paramName, element, update, options)
    {
        this.baseInitialize(element, update, options);
        //this.options.asynchronous  = true;
        this.options.onComplete    = this.onSuccess.bind(this);
        this.options.afterUpdateElement = this.fireListeners.bind(this);
        this.url = url;
        this.paramName = paramName;
    },

    getUpdatedChoices: function()
    {
        this.startIndicator();
        this.options.method = 'get';
        this.options.parameters = new Hash();
        this.options.parameters.set(this.paramName, escape(this.getToken()));
        new Ajax.Request(this.url, this.options);
	},

	onSuccess: function(transport)
	{
        var elements = transport.responseText.evalJSON();
        if(elements)
        {
            this.foundElements = elements;
            var choices = '<ul>';
            for (var i=0; i<elements.length; i++)
            {
                choices += '<li>' + elements[i].displayName + '</li>';
            }
            choices += '</ul>';
            this.updateChoices(choices);
        }
    },
    
    /*
     Method: addListener
     Sets up callback functions to be called when an entry is selected.
     
     Parameters:
     listenerCallback - {function} Will be called with a geoevent containing the JSON properties.
     */
    addListener: function(listenerCallback)
    {
        this.listeners.push(listenerCallback);
    },
    
    fireListeners: function(inputBox, selectedListItem)
    {
        for (var i=0;i<this.listeners.length;i++)
        {
            this.listeners[i](this.foundElements[selectedListItem.autocompleteIndex]);
        }
    }
});
}

/*
 Class: VisStedet.JSONFind
 Non-visual class which contacts an AJAX service, firing a geoevent when a best-case hit is found.
 */
VisStedet.JSONFind = Class.create();
Object.extend(VisStedet.JSONFind.prototype, 
{
    listeners: [],
    url: null,
    displayElement: null,
    running: 0,
    
    /*
     Constructor: VisStedet.JSONFind
     Creates an non-visual object to handle the async communication to a generic JSON search service.
     
     Parameters:
     url - {String} The URL to the JSON service. This can potentially include URL parameters, for example switches for the specific service.
     tag - {String} An optional HTML element which will be displayed and hidden as the AJAX request runs and completes, to use as a "working" indicator.
     */
    initialize: function(urlString, tag)
    {
        this.fireListeners.bind(this);
        this.url = urlString;
        this.displayElement = $(tag);
    },
    
    /*
     Method: addListener
     Sets up callback functions to be called when the search is completed.
     
     Parameters:
     listenerCallback - {function} Will be called with a geoevent containing the JSON properties.
     */
    addListener: function(listenerCallback)
    {
        this.listeners.push(listenerCallback);
    },
    
    /*
     Method: find
     The triggering functionality, initiating the AJAX call, sending the string given as parameter.
     
     Parameters:
     listenerCallback - {function} Will be called with a geoevent containing the JSON properties.
     */
    find: function(input)
    {
        if (input == null || input.length < 4) return;
        if (this.displayElement != null)
        {
            this.running ++;
            this.displayElement.show();
        }
        new Ajax.Request(this.url+"&input=" + escape(input), {
          method: 'get',
          onSuccess: this.fireListeners.bind(this),
          onFailure: function(transport) {
            alert("Ajax error: " + transport.responseText);
          },
          onComplete: this.hideWait.bind(this)
        });
    },
    
    hideWait: function(transport)
    {
        if (this.displayElement != null)
        {
            this.running --;
            if (this.running <=0) this.displayElement.hide();
        }
    },
    
    fireListeners: function(transport)
    {
        var address = transport.responseText.evalJSON();
        for (var i=0;i<this.listeners.length;i++)
        {
            this.listeners[i](address);
        }
    }
});

if(typeof OpenLayers!='undefined')
{
/*
 Class: VisStedet.Map
 An extension to the OpenLayers.Map object to make it understand geoEvents and handle clickable layers implicitly.
 This is a drop-in replacement for the OpenLayers.Map object.
 */
VisStedet.Map = Class.create();
Object.extend(Object.extend(VisStedet.Map.prototype, OpenLayers.Map.prototype), 
{
    clickListeners: [],
    movedListeners: [],
    
    /*
     Constructor: VisStedet.Map
     Creates the fundamental map object.
     
     Parameters:
     div - {String} The HTML element to show the map in.
     options - {Array} An array of configuration objects, as per OpenLayers.
     */
    initialize: function (div, options) {
        var newArguments = [];
        newArguments.push(div, options);
        OpenLayers.Map.prototype.initialize.apply(this, newArguments);
        this.events.register("addlayer", this, this.layerAdded.bind(this));
        this.fractionalZoom = true;
    },

    /*
     Method: setCenterPoint
     Essentially a wrapper around the setCenter function, making it accept the coordinates embedded in a GeoEvent, rather than as an OpenLayers.LonLat object.
     This eases the use, since this function can be hooked up directly as a listener to any object producing GeoEvents, making the map react automatically,
     centering on the raised GeoEvent.

     Parameters:
     geoEvent - {GeoEvent} A standard GeoEvent, assumed to have an x and y field.
     */
    setCenterPoint: function(geoEvent)
    {
        this.panTo(new OpenLayers.LonLat(geoEvent.x, geoEvent.y));
    },
    
    pointChanged: function(geoEvent)
    {
        this.setCenterPoint(geoEvent);
    },
        
    /*
     Method: setBox
     Adjusts the map to show the extent given.

     Parameters:
     geoEvent - {GeoEvent} A standard GeoEvent, assumed to have an minX, minY, maxX and maxY field.
     
     Return:
     GeoEvent - A GeoEvent containing the actual min and max values the map was set to. This can differ from the given values, to preserve the aspect ratio of the map.

     */
    setBox: function(geoEvent)
    {
        this.zoomToExtent(new OpenLayers.Bounds(geoEvent.minX, geoEvent.minY, geoEvent.maxX, geoEvent.maxY));
    	return boundsToGeoEvent(this.getExtent(), "Bounding box");
    },
    
    /*
     Method: addClickListener
     Sets up callback functions to be called when the map generates a point GeoEvent; i.e. when somebody clicks on it.
     
     Parameters:
     listenerCallback - {function} Will be called with a GeoEvent object containing diplayName, x and y.
     */
    addClickListener: function(listenerCallback)
    {
        if (this.clickListeners.length == 0)
        {
            this.events.register("click", this, this.fireClickListeners.bind(this));
        }
        this.clickListeners.push(listenerCallback);
    },
    
    // Deprecated.
    addPointListener: function(listenerCallback)
    {
        this.addClickListener(listenerCallback);
    },
    
    
    /*
     Method: addMovedListener
     Sets up callback functions to be called when the map is panned.
     
     Parameters:
     listenerCallback - {function} Will be called with a GeoEvent object containing minX, maxX, minY and maxY values.
     */
    addMovedListener: function(listenerCallback)
    {
    	if(this.movedListeners.length ==  0)
    	{
	        this.events.register("moveend", this, this.fireMovedListeners.bind(this));
    	}
        this.movedListeners.push(listenerCallback);
    },
        
    layerAdded: function(openLayerEvent)
    {
        // This allows the map to automatically recognize that an newly added layer carries a control object, and install and activate it.
    
        // The 2.5 release does not send the layer object as property of the event; only the map itself.
        // This code will only work from 2.6 onwards, with it's more elegant event model.
        if (typeof(openLayerEvent.layer.control) != 'undefined')
        {
            this.addControl(openLayerEvent.layer.control);
            openLayerEvent.layer.control.activate();
        }
    },
    
    fireClickListeners: function(openLayerEvent)
    {
        var lonlat = this.getLonLatFromViewPortPx(openLayerEvent.xy);
        geoEvent =
        {
            displayName: "Point",
            x: lonlat.lon,
            y: lonlat.lat
        }
        for (var i=0;i<this.clickListeners.length;i++)
        {
            this.clickListeners[i](geoEvent);
        }
    },
    
    fireMovedListeners: function(openLayerEvent)
    {
        geoEvent = boundsToGeoEvent(openLayerEvent.object.getExtent());
        for (var i=0;i<this.movedListeners.length;i++)
        {
            this.movedListeners[i](geoEvent);
        }
    }
});

/*
 Class: VisStedet.AtomFeatureControl
 
 This class is related to the GeoRSS/GeoAtom functionality, and as such deprecated. Use the KML functionality instead.
 
 This class is handler of interaction with a GeoAtom layer. The GeoAtom layer will automatically install it in the map when shown,
 and as such, it should not be necessary to care about thise class in normal use. It is possible, however, that an application will need
 to override this class to provide it's own, customized behaviour.
 The handler is not strictly speaking tied to an Atom feed, but rather to a "vector popup", but since they are always, in Vis Stedet context, seeded from GeoAtom, the naming here remains.
 */
VisStedet.AtomFeatureControl = OpenLayers.Class(OpenLayers.Control, {
    
    multipleKey: null,
    toggleKey: null,
    multiple: false, 
    clickout: true,
    toggle: false,
    hover: false,
    onSelect: function() {},
    onUnselect: function() {},
    geometryTypes: null,
    layer: null,
    callbacks: null,
    selectStyle: null,
    renderIntent: "select",
    //renderIntent: "temporary",
    handler: null,
    previousHover: null,
    listeners: [],

    initialize: function(layer, options) {
        OpenLayers.Control.prototype.initialize.apply(this, [options]);
        this.layer = layer;
        this.callbacks = OpenLayers.Util.extend({
                                                  click: this.clickFeature,
                                                  clickout: this.clickoutFeature,
                                                  over: this.overFeature,
                                                  out: this.outFeature
                                                }, this.callbacks);
        var handlerOptions = { geometryTypes: this.geometryTypes};
        this.handler = new OpenLayers.Handler.Feature(this, layer,
                                                      this.callbacks,
                                                      handlerOptions);
    },

    unselectAll: function(options) {
        // we'll want an option to supress notification here
        var feature;
        for(var i=this.layer.selectedFeatures.length-1; i>=0; --i) {
            feature = this.layer.selectedFeatures[i];
            if(!options || options.except != feature) {
                this.unselect(feature);
            }
        }
    },

    clickFeature: function(feature) {
        /*if(!this.hover)*/ {
            var selected = (OpenLayers.Util.indexOf(this.layer.selectedFeatures,
                                                    feature) > -1);
            if(selected) {
                if(this.toggleSelect()) {
                    this.unselect(feature);
                } else if(!this.multipleSelect()) {
                    this.unselectAll({except: feature});
                }
            } else {
                if(!this.multipleSelect()) {
                    this.unselectAll({except: feature});
                }
                this.select(feature);
                // Fire the VisStedet-specific listeners, which should only fire on a click-select, not a mouseover-select:
                for (var i=0;i<this.listeners.length;i++)
                {
                    var e = {
                        'displayName' : feature.attributes.title,
                        'title' : feature.attributes.title,
                        'summary' : feature.attributes.summary,
                        'link' : feature.attributes.link,
                        'id' : feature.fid,
                        'x' : ((feature.geometry.bounds.left + feature.geometry.bounds.right)/2),
                        'y' : ((feature.geometry.bounds.top + feature.geometry.bounds.bottom)/2)
                    }
                
                    this.listeners[i](e);
                }
            }
        }
    },

    multipleSelect: function() {
        return this.multiple || this.handler.evt[this.multipleKey];
    },
    
    toggleSelect: function() {
        return this.toggle || this.handler.evt[this.toggleKey];
    },

    clickoutFeature: function(feature) {
        if(/*!this.hover &&*/ this.clickout) {
            this.unselectAll();
        }
    },

    overFeature: function(feature) {
        if (feature != this.previousHover)
        {
            if (this.previousHover)
            {
                if(OpenLayers.Util.indexOf(this.layer.selectedFeatures, this.previousHover) == -1)
                    this.layer.drawFeature(this.previousHover, "default");
                else
                    this.layer.drawFeature(this.previousHover, "select");
            }
        }
        this.layer.drawFeature(feature, 'temporary');
        this.previousHover = feature;
        this.onSelect(feature);
        
        return;
        if(/*this.hover &&*/
           (OpenLayers.Util.indexOf(this.layer.selectedFeatures, feature) == -1))
        {
            this.select(feature, 'temporary');
            this.previousHover = feature;
        }
    },

    outFeature: function(feature) {
    /*
    if(OpenLayers.Util.indexOf(this.layer.selectedFeatures, feature) > -1)
        this.layer.drawFeature(feature, "select");
    else
        this.layer.drawFeature(feature, "default");
        */
        /*if(this.hover)*/ {
            //this.unselect(feature);
        }
    },
    
    select: function(feature, style) {
        this.layer.selectedFeatures.push(feature);

        var selectStyle = style || this.selectStyle || this.renderIntent;
        
        this.layer.drawFeature(feature, selectStyle);
        this.layer.events.triggerEvent("featureselected", {feature: feature});
        this.onSelect(feature);
    },

    unselect: function(feature) {
        // Store feature style for restoration later
        this.layer.drawFeature(feature, "default");
        OpenLayers.Util.removeItem(this.layer.selectedFeatures, feature);
        this.layer.events.triggerEvent("featureunselected", {feature: feature});
        this.onUnselect(feature);
    },

    setMap: function(map) {
        this.handler.setMap(map);
        OpenLayers.Control.prototype.setMap.apply(this, arguments);
    },

    /*
     Method: addListener
     Sets up callback functions to be called when a feature is clicked. As per this entire class being largely an internal part, it is generally not necessary (nor recommended) to access this function directly.
     The PopupVector layer contains functions to handle the events (which internally uses this function).
     
     Parameters:
     listenerCallback - {function} Will be called with a GeoEvent representing the clicked-on feature, containing displayName, title, summary, link, id, x and y.
     */
    addListener: function(listenerCallback)
    {
        this.listeners.push(listenerCallback);
    }
});

/*
 Class: VisStedet.PopupVector
 
 This class is related to the GeoRSS/GeoAtom functionality, and as such deprecated. Use the KML functionality instead.

 Extension of the basic vector layer to provide pop-up bubbles. It relies on the VisStedet.AtomFeatureControl handler to track the interaction.
 */
VisStedet.PopupVector = OpenLayers.Class(OpenLayers.Layer.Vector, {

    listeners: [],
    control: null,
    previousFeature: null,
    url: null,
    loaded: false,
    format: null,

    /*
     Constructor: VisStedet.PopupVector
     This laregly reflects the OpenLayers.GML class, in the sense that it's an "external file handler" for the Vector layer class.
     However, it defaults to the GeoAtom format rather than GML, and adds the Vis Stedet event model and pop-ups.
     
     Parameters:
     name - {String} 
     url - {String} URL of an Atom file.
     options - {Object} Hashtable of extra options to tag onto the layer.
     */
     initialize: function(name, url, options) {
        var newArguments = [];
        newArguments.push(name, options);
        OpenLayers.Layer.Vector.prototype.initialize.apply(this, newArguments);
        this.url = url;

        this.control = new VisStedet.AtomFeatureControl(this, {onSelect: this.onFeatureSelect, hover: true});
        
        //this.control = new OpenLayers.Control.SelectFeature(this, {onSelect: this.onFeatureSelect, hover: true});
    },
       
    setVisibility: function(visibility, noEvent) {
        OpenLayers.Layer.Vector.prototype.setVisibility.apply(this, arguments);
        if(this.visibility && !this.loaded){
            this.load();
        }
    },
    
    moveTo: function(bounds, zoomChanged, minor) {
        OpenLayers.Layer.Vector.prototype.moveTo.apply(this, arguments);
        if(this.visibility && !this.loaded){
            this.events.triggerEvent("loadstart");
            this.load();
        }
    },

    load: function() {
        if (!this.loaded) {
            var results = OpenLayers.loadURL(this.url, null, this, this.requestSuccess, this.requestFailure);
            this.loaded = true;
        }
    }, 

    requestSuccess: function(request) {
        var doc = request.responseXML;
        
        if (!doc || request.fileType!="XML") {
            doc = request.responseText;
        }

        var feed = this.format ? new this.format() : new VisStedet.GeoAtom();
        this.addFeatures(feed.read(doc));
        this.events.triggerEvent("loadend");
    },
    
    requestFailure: function(request) {
        alert("Error in loading feed file "+this.url);
        this.events.triggerEvent("loadend");
    },

    onFeatureSelect: function(feature)
    {
        if (this.previousFeature != null)
        {
            this.map.removePopup(this.previousFeature.popup);
            this.previousFeature.popup.destroy();
            this.previousFeature.popup = null;
            this.previousFeature = null;
        }
        
        var popup = new OpenLayers.Popup.AnchoredBubble("id", 
            feature.geometry.getBounds().getCenterLonLat(),
            null,
            '<b>' + feature.attributes.title + '</b><br/>' + feature.attributes.summary,
            null, true);
        popup.setOpacity(.9);
        feature.popup = popup;
        this.map.addPopup(popup);
        this.previousFeature = feature;
    },
    
    /*
     Method: addListener
     Sets up callback functions to be called when a feature shown in this layer is clicked.
     
     Parameters:
     listenerCallback - {function} Will be called with a GeoEvent representing the clicked-on feature, containing displayName, title, summary, link, id, x and y.
     */
    addListener: function(listenerCallback)
    {
        // Pipe the listener to the actual feature control, which is where we can handle the detailed click differentiation:
        this.control.addListener(listenerCallback);
    }

});

/*
 Class: VisStedet.GeoAtom

 This class is related to the GeoRSS/GeoAtom functionality, and as such deprecated. Use the KML functionality instead.
 
 A parser for the GeoAtom feed. It is usually not necessary to use this class directly, since it will internally be used by default by the PopupVector layer class.
 */
VisStedet.GeoAtom = OpenLayers.Class(OpenLayers.Format.XML, {
    
    featureTitle: "Untitled",
    
    featureDescription: "No Description",
    
    /*
     * Constructor: VisStedet.GeoAtom
     * Create a new parser for GeoAtom.
     *
     * Parameters:
     * options - {Object} An optional object whose properties will be set on
     *     this instance.
     */
    initialize: function(options) {
        OpenLayers.Format.XML.prototype.initialize.apply(this, [options]);
    },
    
    /**
     * createGeometryFromItem
     * Return a geometry from a GeoRSS Item.
     *
     * InternalParameters:
     * item - {DOMElement} A GeoRSS item node or Atom entry
     *
     * InternalReturns:
     * {<OpenLayers.Geometry>} A geometry representing the node.
     */
    createGeometryFromItem: function(item) {
    
        var point = this.getElementsByTagNameNS(item, '*', 'Point');
        var line = this.getElementsByTagNameNS(item, '*', 'LineString');
        var polygon = this.getElementsByTagNameNS(item, '*', 'Polygon');
        
        var coords = this.getElementsByTagNameNS(item, '*', 'posList');
        if (coords.length == 0) coords = this.getElementsByTagNameNS(item, '*', 'coordinates');
        if (coords.length == 0) coords = this.getElementsByTagNameNS(item, '*', 'coordList');
        if (coords.length == 0) coords = this.getElementsByTagNameNS(item, '*', 'pos');

        var coordList = OpenLayers.String.trim(coords[0].firstChild.nodeValue).split(/[\s+,]/);
        
        var components = []; 
        for (var i=0; i < coordList.length; i+=2) {
            var point = new OpenLayers.Geometry.Point(parseFloat(coordList[i]), parseFloat(coordList[i+1]));
            components.push(point);
        }
        
        if (polygon.length > 0) return new OpenLayers.Geometry.Polygon([new OpenLayers.Geometry.LinearRing(components)]);            
        if (line.length > 0) return new OpenLayers.Geometry.LineString(components);            
        if (point.length > 0) return new OpenLayers.Geometry.Point(components[0]);            
        
        alert('No recognized GML element in entry');
        return;        
    },        

    /**
     * createFeatureFromItem
     * Return a feature from a GeoRSS Item.
     *
     * Parameters:
     * item - {DOMElement} A GeoRSS item node.
     *
     * Returns:
     * {<OpenLayers.Feature.Vector>} A feature representing the item.
     */
    createFeatureFromItem: function(item) {
    
        var geometry = this.createGeometryFromItem(item);
        
        /* Provide defaults for title and description */
        var title = this.getChildValue(item, "*", "title", this.featureTitle);
       
        /* First try RSS descriptions, then Atom summaries */
        var summary = this.getChildValue(
            item, "*", "description",
            this.getChildValue(item, "*", "summary", this.featureDescription)
        );

        /* If no link URL is found in the first child node, try the
           href attribute */
        var link = this.getChildValue(item, "*", "link");
        if(!link) {
            try {
                link = this.getElementsByTagNameNS(item, "*", "link")[0].getAttribute("href");
            } catch(e) {
                link = null;
            }
        }

        var id = this.getChildValue(item, "*", "id", null);
        var data = {
            "title": title,
            "summary": summary,
            "link": link
        };
        var feature = new OpenLayers.Feature.Vector(geometry, data);
        feature.fid = id;
        return feature;
    },        
    
    /*
     * getChildValue
     *
     * Parameters:
     * node - {DOMElement}
     * nsuri - {String} Child node namespace uri ("*" for any).
     * name - {String} Child node name.
     * def - {String} Optional string default to return if no child found.
     *
     * Returns:
     * {String} The value of the first child with the given tag name.  Returns
     *     default value or empty string if none found.
     */
    getChildValue: function(node, nsuri, name, def) {
        var value;
        try {
            value = this.getElementsByTagNameNS(node, nsuri, name)[0].firstChild.nodeValue;
        } catch(e) {
            value = (def == undefined) ? "" : def;
        }
        return value;
    },
    
    /*
     Method: read
     Return a list of features from a GeoAtom document.
     
     Parameters:
     data - {Element} 
     
     Returns:
     An Array of <OpenLayers.Feature.Vector>s
     */
    read: function(doc) {
        if (typeof doc == "string") { 
            doc = OpenLayers.Format.XML.prototype.read.apply(this, [doc]);
        }

        /* Try Atom entries first, then RSS items */
        var itemlist = null;
        itemlist = this.getElementsByTagNameNS(doc, '*', 'entry');
        if (itemlist.length == 0) {
            itemlist = this.getElementsByTagNameNS(doc, '*', 'item');
        }
        
        var numItems = itemlist.length;
        var features = new Array(numItems);
        for(var i=0; i<numItems; i++) {
            features[i] = this.createFeatureFromItem(itemlist[i]);
        }
        return features;
    }
});

/*
 Class: VisStedet.KMLLayer
 
 Extension of the OpenLayers GML layer object, setting up defaults to handle KML and pass geoEvents rather than "raw" GML feature select events.
 */
VisStedet.KMLLayer = Class.create();
Object.extend(Object.extend(VisStedet.KMLLayer.prototype, OpenLayers.Layer.GML.prototype), 
{
    listeners: [],
    
    /*
     Constructor: VisStedet.KMLLayer
     
     Parameters:
     name - {String} The name of the layer, as it appears in, for example, the layer switcher.
     url - {string} The URL to a KML file.
     */
    initialize: function (name, url) {
        var newArguments = [];
        this.fireListeners = this.fireListeners.bind(this);
        this.control = new OpenLayers.Control.SelectFeature(this, {onSelect: this.fireListeners});
        newArguments.push(name, url);
        newArguments.push(
        {
            format: OpenLayers.Format.KML,
            formatOptions: {
                extractStyles: true,
                extractAttributes: true
            }
        });
        this.listeners=[];
        OpenLayers.Layer.GML.prototype.initialize.apply(this, newArguments);
    },

    /*
     Method: addListener
     Sets up a callback function to call when a feature (placemark) in the KML layer is clicked.
     
     Parameters:
     listenerCallback - {function} A function to be called with a GeoEvent, carrying the information from the placemark, in addition to the x and y.
     */
    addListener: function(listenerCallback)
    {
        this.listeners.push(listenerCallback);
    },
    
    fireListeners: function(e)
    {
        e.x = e.geometry.x;
        e.y = e.geometry.y;
        e.displayName = e.layer.name;
        
        for (var i=0;i<this.listeners.length;i++)
        {
            this.listeners[i](e);
        }
    }
});
}

/*
 Class: VisStedet.AddressSearchbox
 
 Extension of an autocompleter, hooking up to FindAddress2. This one is using JSONP, and so will work without any proxy code.
 */
if(typeof Autocompleter!='undefined')
{
VisStedet.AddressSearchbox = Class.create();
Object.extend(Object.extend(VisStedet.AddressSearchbox.prototype, Autocompleter.Base.prototype),
    {
    listeners: [],
    currentList: [],
    choices: {},

    bgCol: '#ffffff',
    /*
     Constructor: VisStedet.AddressSearchbox
     
     Parameters:
     element - {String} The id of a div element in the dom structure.
     paramObject - {Object} Object containing additional parameters for the searchbox. workingCallback is a function which will be called back with true or false as the autocompleter waits for answer from the URL. addressCallback is a function to be called when an unique address is selected in the search box. This can also be added with the addListener function.
    */
    initialize: function(element, paramObject)
        {
        if(paramObject)
            {
            this.workingCallback = paramObject.workingCallback;
            if (paramObject.addressCallback)
                {
                if (paramObject.addressCallback[0])
                    {
                    this.listeners = paramObject.addressCallback;
                    }
                else
                    {
                    this.listeners[0] = paramObject.addressCallback;
                    }
                }
            }
        this.choices = new Element('div', {id: $(element).id+'_choices'});
        this.choices.style.zIndex = 1000;
        this.choices.className = $(element).className;
        $(element).insert({after: this.choices});     
        this.bgCol = $(element).style.backgroundColor;
        var options =
            {
            trigger: this.fireListeners.bind(this),
            updateFunction: this.getUpdatedChoices.bind(this),
            minChars: 3,
            frequency: .2,
            updateElement: function(entry)
                {
                a = currentList[entry.autocompleteIndex];
                
                if (a.hasStreetBuildingIdentifier)
                    {
                    $(element).value = a.displayName;
                    setCaret($(element), (a.streetName + a.streetBuildingIdentifier).length + 1);
                    }
                else
                    {
                    $(element).value = a.displayName.substring(0, a.streetName.length) +' ' + a.displayName.substring(a.streetName.length);
                    setCaret($(element), a.streetName.length + 1);
                    }
                if (a.isValidated) 
                    {
                    this.trigger.defer(a);
                    }
                else
                    {
                    if (a.hasPostCode)
                        {
                        this.updateFunction.delay(.25, a);
                        }
                    }
                }
            };
        this.baseInitialize(element, this.choices, options);
        },
  
    getUpdatedChoices: function(address)
        {
        if (this.workingCallback) this.workingCallback(true);
        var search;
        if (address)
            search = address.displayName;
        else
            search = this.getToken();
        JSONP.call('http://visstedet.kms.dk/FindAddress2.aspx?input=' + Url.encode(search), function(data)
            {
            currentList = data;
            if (this.workingCallback) this.workingCallback(false);
            
            if (data.length == 0)
                {
                this.element.style.backgroundColor = "#ff8888";
                this.choices.hide();
                }
            else
                {
                this.element.style.backgroundColor = this.bgCol;
                if (data.length == 1 && data[0].isValidated)
                    {
                    this.fireListeners(data[0]);
                    this.choices.hide();
                    }
                else
                    {
                    var choicesHTML = '<ul>';
                    if (data)
                        {
                        for (var i=0; i<data.length; i++)
                            {
                            choicesHTML += '<li>' + data[i].displayName + '</li>';
                            }
                        }
                    choicesHTML += '</ul>';

                    // Ugly workaround for a known issue where IE fails on the first insert. (Setting the style before the autocompleter has been inserted into the DOM).
                    // This hack is only necessary when using a "transitional" doctype, triggering IE quirks/compatible mode.
                    try
                        {
                        this.updateChoices(choicesHTML);
                        }
                    catch(e)
                        {
                        this.updateChoices(choicesHTML);
                        }
                    }
                }
            }.bind(this), true);
        },
        
    addListener: function(listenerCallback)
    {
        this.listeners.push(listenerCallback);
    },
            
    fireListeners: function(a)
        {
        for (i=0;i<this.listeners.length;i++)
            {
            this.listeners[i](a);
            }
        }
    });
}

var JSONP =
    {
    scriptNodes: {},
    jsonpCallbacks: {},
    jsonpTimeouts: {},
    jsonpTimeoutIds: {},
    runner: 0,
    call: function(url, callback, ignoreOld, timeout, errorCallback)
        {
        if (!timeout)timeout = 5000;
	    ++this.runner;
	    this.jsonpCallbacks[this.runner] = function(i, o)
	        {
			document.getElementsByTagName('head')[0].removeChild(this.scriptNodes[i]);
			delete this.scriptNodes[i];
			window.clearTimeout(this.jsonpTimeoutIds[i]);
			delete this.jsonpTimeouts[i];
			delete this.jsonpTimeoutIds[i];
			if(!ignoreOld || i>=this.runner) callback(o);
		    }.bind(this, this.runner);

        this.jsonpTimeouts[this.runner] = function(i, error, url)
            {
            this.jsonpCallbacks[i]= function(i) {delete this.jsonpCallbacks[i];}.bind(this, i);
			document.getElementsByTagName('head')[0].removeChild(this.scriptNodes[i]);
			delete this.scriptNodes[i];
			delete this.jsonpTimeouts[i];
			delete this.jsonpTimeoutIds[i];
			if (error) error(url + ' timed out');
		    }.bind(this, this.runner, errorCallback, url);

	    this.jsonpTimeoutIds[this.runner] = window.setTimeout(this.jsonpTimeouts[this.runner], timeout);

	    if (url.indexOf('?') >-1)
		    url += '&';
	    else
		    url += '?';
	    url+= 'jsonp=JSONP.jsonpCallbacks[' + this.runner + ']';

	    this.scriptNodes[this.runner] = document.createElement('script');
        this.scriptNodes[this.runner].type = 'text/javascript';
        this.scriptNodes[this.runner].src = url;
	    document.getElementsByTagName('head')[0].appendChild(this.scriptNodes[this.runner]);
        }
    }

function setCaret (element, pos)
    {
    if (element.selectionStart || element.selectionStart == '0')
        {
        element.selectionStart = pos;
        element.selectionEnd = pos;
        element.focus ();
        }
    else if (document.selection)
        {
        element.focus();
        var range = element.createTextRange();
        range.collapse(true);
        range.moveEnd('character', pos);
        range.moveStart('character', pos);
        range.select();
        }
    }

/**
*
*  URL encode / decode
*  http://www.webtoolkit.info/
*
**/
 
var Url = {
 
	// public method for url encoding
	encode : function (string) {
		return escape(this._utf8_encode(string));
	},
 
	// public method for url decoding
	decode : function (string) {
		return this._utf8_decode(unescape(string));
	},
 
	// private method for UTF-8 encoding
	_utf8_encode : function (string) {
		string = string.replace(/\r\n/g,"\n");
		var utftext = "";
 
		for (var n = 0; n < string.length; n++) {
 
			var c = string.charCodeAt(n);
 
			if (c < 128) {
				utftext += String.fromCharCode(c);
			}
			else if((c > 127) && (c < 2048)) {
				utftext += String.fromCharCode((c >> 6) | 192);
				utftext += String.fromCharCode((c & 63) | 128);
			}
			else {
				utftext += String.fromCharCode((c >> 12) | 224);
				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
				utftext += String.fromCharCode((c & 63) | 128);
			}
 
		}
 
		return utftext;
	},
 
	// private method for UTF-8 decoding
	_utf8_decode : function (utftext) {
		var string = "";
		var i = 0;
		var c = c1 = c2 = 0;
 
		while ( i < utftext.length ) {
 
			c = utftext.charCodeAt(i);
 
			if (c < 128) {
				string += String.fromCharCode(c);
				i++;
			}
			else if((c > 191) && (c < 224)) {
				c2 = utftext.charCodeAt(i+1);
				string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
				i += 2;
			}
			else {
				c2 = utftext.charCodeAt(i+1);
				c3 = utftext.charCodeAt(i+2);
				string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
				i += 3;
			}
 
		}
		return string;
	} 
}

/*
 Class: Helper
 */
 /*
 Method: showObject
 A debug helper function to show the fields of an object.
 
 Parameters:
 obj - {Object} The object to display in an alert box.
 */
function showObject(obj)
{
    var result = "";
    for (var prop in obj)
    {
        if (typeof(obj[prop]) != 'function')
            result += typeof obj + "." + prop + " = " + obj[prop] + " (" + typeof(obj[prop])+")\n";
    }
    alert(result);
}

 /*
 Method: boundsToGeoEvent
 Helper functionality to convert an OpenLayers bounds object into the GeoObject notation.
 
 Parameters:
 bounds - {OpenLayers.Bounds} The bounds to convert.
 displayName - {string} The displayName for the resulting GeoEvent.

 Returns:
 An geoevent object carrying the same minX, minY, maxX, maxY values as expressed in the Bounds object.
 */
function boundsToGeoEvent(bounds, displayName)
{
		var box = bounds.toArray();
		var center = bounds.getCenterLonLat();
    	return {
    		displayName: displayName,
    		minX: box[0],
    		minY: box[1],
    		maxX: box[2],
    		maxY: box[3],
    		x: center.lon,
    		y: center.lat
    	}
}

/*
 Class: Cookie
 Convenience functions to handle cookies. Why this is not a part of the javascript core itself is a mystery.
 */
var Cookie = {
  set: function(name, value, daysToExpire) {
    var expire = '';
    if (daysToExpire != undefined) {
      var d = new Date();
      d.setTime(d.getTime() + (86400000 * parseFloat(daysToExpire)));
      expire = '; expires=' + d.toGMTString();
    }
    return (document.cookie = escape(name) + '=' + escape(value || '') + expire);
  },
  get: function(name) {
    var cookie = document.cookie.match(new RegExp('(^|;)\\s*' + escape(name) + '=([^;\\s]*)'));
    return (cookie ? unescape(cookie[2]) : null);
  },
  erase: function(name) {
    var cookie = Cookie.get(name) || true;
    Cookie.set(name, '', -1);
    return cookie;
  },
  accept: function() {
    if (typeof navigator.cookieEnabled == 'boolean') {
      return navigator.cookieEnabled;
    }
    Cookie.set('_test', '1');
    return (Cookie.erase('_test') === '1');
  }
};


/*
 Class: Defaults
 
 In principle, utility functions to ease configuration of the "actual" functionality. Practically, the easy access to reasonable defaults is
 a large part of Vis Stedet, and these functions also demonstrates "best practice" to use the components.
 */
 
/*
 Method: getDropdown
 Call this to wire the input and div tags for a place search to actual AJAX functionality.
 This configures the generic JSON dropdown functionality to use the specific JSON service.
 
 Parameters:
 elementID - {String} The DOM ID of the input tag.
                     It is assumed that a corresponding 'elementID_choices' is available in the HTML
                     to show the list of choices.
                     If an 'elementID_indicator' is available in the HTML, it's display property will be toggled
                     on and off as the async AJAX call is running and returning.
 minChars  {Integer} The minimum number of chars to type before the AJAX request is fired. Defaults to 4 if not given.
 
 Returns:
 JSONDropdown - A bound instance of a VisStedet.JSONDropdown, to hook event listener functions into.  
 */
function getDropdown(elementId, url, minChars)
{
	var params = {};
    if (minChars != null )
        params.minChars  = minChars;
    else
        params.minChars  = 4;
	if ($(elementId + '_indicator') != null) params.indicator = elementId + '_indicator';
    return new VisStedet.JSONDropdown(url, 'input', elementId, elementId + '_choices', params);
}

/*
 Method: getPlaceGazetteerDropdown
 This is an internal helper function, preserved for ease of legacy support.
 */
function getPlaceGazetteerDropdown(elementId, minChars)
{
    return getDropdown(elementId, "/Proxy.aspx?path=GetPlaceJSON.aspx", minChars);
}

/*
 Method: getAddressDropdown
 Another internal legacy support.
 */
function getAddressDropdown(elementId, minChars)
{
    return getDropdown(elementId, '/Proxy.aspx?path=GetAddressJSON.aspx&wildcard=true', minChars);
}

function getAWSFind()
{
   return new VisStedet.JSONFind("/Proxy.aspx?path=FindAddressJSON.aspx");
}

/*
 Method: getDefaultMap
 Sets up a default map. This is a conveniencem method to get something on the screen quickly,
 and is quite likely to be replaced by customized initialization methods.
 
 elementID - {String} The DOM ID of the div tag to contain the map.
 showPostDistricts - {boolean} Whether to show the post districts overlay or not.
 
 Return:
 OpenLayers.Map - An instantiated OpenLayers object bound to the given div tag.
 */
function getDefaultMap(elementId, showPostDistricts, login, password)
{
    var map = new VisStedet.Map(
        elementId,
        {
            projection: 'EPSG:25832',
            units: 'Meters',
            maxExtent: new OpenLayers.Bounds(100000,6000000,1000000,6420000),
            resolutions: new Array(.5, 1, 2, 4, 8, 16, 32, 64, 128, 256, 500, 1000),
            controls: []
        }
    );

    map.addLayer(
        new OpenLayers.Layer.WMS(
            "DTK/Skærmkort", 
            "http://kortforsyningen.kms.dk/",
            {
                servicename: 'topo_skaermkort',
		        layers: '0,1,2,3,4,5,6,7,8,9,10,11,12,13,14',
                //servicename: 'dtk_skaermkort',
                //layers: 'dtk_skaermkort',
                format: 'image/jpeg',
                ticket: Cookie.get('kfticket')
             },
             {
                isBaseLayer: true,
                buffer: 1,
                displayInLayerSwitcher: true
             }
        )
    );
    
    if(showPostDistricts)
    {
        map.addLayer(
            new OpenLayers.Layer.WMS(
                "Postdistrikter", 
                "http://kortforsyningen.kms.dk/",
                {
                    servicename: 'kms_vector_basic_01',
                    layers: 'POSTDISTRIKT,POSTDISTRIKTNAVN',
                    format: 'image/png',
                    transparent: 'true',
                    ignoreillegallayers: 'true',
                    ticket: Cookie.get('kfticket')
                 },
                 {
                    singleTile: true,
                    buffer: 0
                 }
            )
        );
    }
    
    map.addControl(new OpenLayers.Control.PanZoomBar());
    map.addControl(new OpenLayers.Control.Navigation());
    
    map.fractionalZoom = true;

    if(showPostDistricts) map.addControl(new OpenLayers.Control.LayerSwitcher());

	map.setCenter(new OpenLayers.LonLat(722110, 6178883));
	map.zoomTo(10);
	
	return map;
}

/*
 Method: getDefaultWMTSMap
 Sets up a default map using the WMTS tile server. This is a conveniencem method to get something on the screen quickly,
 and is quite likely to be replaced by customized initialization methods.
 
 elementID - {String} The DOM ID of the div tag to contain the map.
 x - {number} Optional x coordinate, in EPSG:25832.
 y - {number}  Optional y coordinate, in EPSG:25832.
 zoom - {integer} Optional zoom level.
 
 Return:
 OpenLayers.Map - An instantiated OpenLayers object bound to the given div tag.
 */
function getDefaultWMTSMap(elementId, x, y, zoom)
{
	var map = new OpenLayers.Map({
		div: 'map',
		projection: 'EPSG:25832',
		units: 'm',
		maxExtent: new OpenLayers.Bounds(120000.0, 5661139.2, 1000000, 6500000.0),
		maxResolution: 1638.4,
		numZoomLevels: 12,
		controls : []
	});
	
	var wmts = new OpenLayers.Layer.WMTS({
		name: "WMTS",
		url: ["http://a.kortforsyningen.kms.dk/topo_skaermkort", "http://b.kortforsyningen.kms.dk/topo_skaermkort", "http://c.kortforsyningen.kms.dk/topo_skaermkort"],
		style: "default",
		layer: "dtk_hillshade",
		matrixSet: "View1",
		format: "image/jpeg",
		params: {
			ticket: Cookie.get('kfticket')
		},
		matrixIds: [
			{identifier: "L00", scaleDenominator: 1638.4/0.00028},
			{identifier: "L01", scaleDenominator: 819.2/0.00028},
			{identifier: "L02", scaleDenominator: 409.6/0.00028},
			{identifier: "L03", scaleDenominator: 204.8/0.00028},
			{identifier: "L04", scaleDenominator: 102.4/0.00028},
			{identifier: "L05", scaleDenominator: 51.2/0.00028},
			{identifier: "L06", scaleDenominator: 25.6/0.00028},
			{identifier: "L07", scaleDenominator: 12.8/0.00028},
			{identifier: "L08", scaleDenominator: 6.4/0.00028},
			{identifier: "L09", scaleDenominator: 3.2/0.00028},
			{identifier: "L10", scaleDenominator: 1.6/0.00028},
			{identifier: "L11", scaleDenominator: 0.8/0.00028}
		],
		isBaseLayer : true,
		displayInLayerSwitcher : true,
		transitionEffect : 'resize'
	});

	map.addLayer(wmts);

	// The buttons and bar in upper left corner:
	map.addControl(new OpenLayers.Control.PanZoomBar());

	// All mouse controls (drag, zoom with mouse wheel. etc)
	map.addControl(new OpenLayers.Control.Navigation());

	// Layer selector in upper right corner. (Base layers show as choices between
	// radio buttons, other layers as selectable checkboxes):
	map.addControl(new OpenLayers.Control.LayerSwitcher());
	
	// Default center and zoom on Copenhagen if nothing else is given:
	if (!x) x = 724500;
	if (!y) y = 6176450;
	if (!zoom) zoom = 10;

	map.setCenter(new OpenLayers.LonLat(x, y), zoom);
	
	return map;
}