/* Build date: 20100707 1106 *//**
 * @fileOverview All central functionality related to HMap.
 */

/**
 * Creates a map object. HMap.setCenter() should be called immediately after
 * creating a map object to initialize it.
 *
 * @class HMap is the central class of the API and is instantiated in order to
 * create a map.
 *
 * @param {Element} map Map container element.
 * @constructor
 */
function HMap(map) {
    var me = this;
    me.map = map;

    // initialize globals
    me.globals = {
        isInitialized: false,
        isDragging: false,
        draggingEnabled: true,  // map dragging is enabled by default
        doubleClickZoomEnabled: true,  // double click zoom is enabled by default
        mouseWheelZoomEnabled: true,    // mouse wheel zoom is enabled by default
        exclusiveInfoBoxEnabled: true,
        startX: 0,              // start pixel when dragging
        startY: 0,              // start pixel when dragging
        deltaX: 0,              // delta distance when dragging
        deltaY: 0,              // delta distance when dragging
        mapSize: null,
        mapPaddingX: 0,         // pixels of padding tiles outside visible map
        mapPaddingY: 0,         // pixels of padding tiles outside visible map
        mapPosition: null,
        tilesPerRow: 0,
        tilesPerColumn: 0,
        tileInitOffsetX: 0,
        tileInitOffsetY: 0,
        tileCount: 0,
        mapRow: 0,              // mapstore tile row for tile in lower left corner
        mapColumn: 0,           // mapstore tile column for tile in lower left corner
        resolution: 0,
        zoomLevel: 0,
        gridContainer: null,
        overlayContainer: null,
        rightClickTimestamp: 0,
        panIntervalCount: 0,
        panDestination: null,
        panCurrent: null,
        polyLayer: null,
        currentMousePos: null,
        paddingLeft: 0,
        paddingTop: 0,
        paddingBottom: 0,
        paddingRight: 0,
        hiddenInputElement: null,
        centerPoint: null       // remember set center point to avoid resolution errors
    };
    var G = me.globals;

    // load excanvas if IE
    if (HMapHelpers.isIE()) {
        HMapHelpers.loadScript("http://www.hitta.se/js/excanvas.compressed.js");
    }

    // array containing all overlay objects (HOverlay)
    me.overlays = [];

    // array containing all control objects (HControl)
    me.controls = [];

    // allow overflow on map container
    HMapHelpers.applyStyle([me.map], {
        overflow: "hidden",
        position: "relative",
        outline: "none" // disable dotted-focus border in FF
    });
    
    // allow keyboard event to be received by div in Firefox (note: if XHTML
    // then attribute must be "tabindex" (all lowercase)).
    me.map.setAttribute("tabIndex", 0);
    me.map.setAttribute("hideFocus", true); // disable dotted-focus border in IE

    G.innerContainer = HMapHelpers.createContainerDiv(me.map);

    // container for map tiles
    G.gridContainer = HMapHelpers.createContainerDiv(G.innerContainer);
    HMapHelpers.applyStyle([G.gridContainer], {zIndex: 0});

    // container for canvas used by polylayer
    G.polyContainer = HMapHelpers.createContainerDiv(G.innerContainer);

    // container for overlay objects (markers, infobox, etc)
    G.overlayContainer = HMapHelpers.createContainerDiv(G.innerContainer);
    HMapHelpers.applyStyle([G.overlayContainer], {zIndex: HOverlay.Z_INDEX_OFFSET});

    G.polyLayer = new HPolyLayer(this);

    me.transform = new HTransform();

    // map provider manager
    me.mapProviderManager = new HMapProviderManager(this);

    this.mapProviderManager.add(new HStandardMapProvider());
//    this.mapProviderManager.add(new StreetViewDotsMapProvider());
//    this.mapProviderManager.add(new ChessMapProvider());
//    this.mapProviderManager.add(new DebugMapProvider());

    // attach events to enable dragging of map (within clousures to keep context)
    HEvent.addDomListener(document, "mousemove", function(e) {me.mouseMove(e);});
    HEvent.addDomListener(document, "mouseup", function(e) {me.mouseUp(e);});

    HEvent.addDomListener(G.innerContainer, "mousemove", function(e) {me.mouseMoveMap(e);});
    HEvent.addDomListener(G.innerContainer, "mousedown", function(e) {me.mouseDown(e);});
    HEvent.addDomListener(G.innerContainer, "mousewheel", function(e) {return me.mouseWheel(e);});
    HEvent.addDomListener(G.innerContainer, "click", function(e) {me.mouseClick(e);});
    HEvent.addDomListener(G.innerContainer, "dblclick", function(e) {me.mouseDblClick(e);});

    HEvent.addDomListener(G.innerContainer, "dragstart", HEvent.stopEvent);
    HEvent.addDomListener(G.innerContainer, "selectstart", HEvent.stopEvent);
    HEvent.addDomListener(G.innerContainer, "contextmenu", HEvent.stopEvent);

    // keyboard listeners
    if (HMapHelpers.isSafari()) {
        // Safari hidden input box, see comment in setFocus() for details
        var inputElement = document.createElement("input");
        inputElement.id = "mapinput";

        HMapHelpers.applyStyle([inputElement], {
            border: "none",
            outline: "none",
            width: "0px",
            height: "0px",
            fontSize: "0px"
        });

        me.map.appendChild(inputElement);
        G.hiddenInputElement = inputElement;

        HEvent.addDomListener(inputElement, "keydown", function(e) {return me.keyAction(e);});
    }else {
        HEvent.addDomListener(me.map, "keydown", function(e) {return me.keyAction(e);});
    }
}

/**
 * Array specifying resolution per zoom level.
 * @constant
 * @private
 */
HMap.ZOOM_LEVELS = [0, 3500, 700, 200, 70, 25, 10, 4, 2, 0.5, 0.2];

/**
 * RT90 x-coordinate for map row 0
 * @constant
 * @private
 */
HMap.RT90_BOTTOM_LEFT_GEO_POINT_NORTH = 5651424;

/**
 * RT90 y-coordinate for map column 0
 * @constant
 * @private
 */
HMap.RT90_BOTTOM_LEFT_GEO_POINT_EAST = 451424;

/**
 * RT90 x-coordinate for point above available map (used for overlay
 * z-index calculations)
 * @constant
 * @private
 */
HMap.RT90_GEO_POINT_NORTH = 8200000;

/**
 * Default zoom level if unset when initializing.
 * @constant
 * @private
 */
HMap.DEFAULT_ZOOM_LEVEL = 4;

/**
 * Extra tiles out of view to pad map with.
 * @constant
 * @private
 */
HMap.TILE_PADDING = 2;

/**
 * Speed of pan animation (higher = faster). May be capped by
 * PAN_MAX_STEP_SIZE_PART depending on value.
 * @constant
 * @private
 */
HMap.PAN_ANIMATION_SPEED = 7;

/**
 * Max step to move per animation "frame" when panning (higher = faster).
 * @constant
 * @private
 */
HMap.PAN_MAX_STEP_SIZE_PART = 12;

/**
 * Padding of bounds when to perform smooth panning instead of relocating
 * whole map when using panTo().
 * @constant
 * @private
 */
HMap.PAN_TO_SMOOTH_SCROLL_PADDING = 1.5;

/**
 * How much to pan the map each time (e.g. factor = 2/3 moves the map 2/3
 * of it's size in that direction)
 * @constant
 * @private
 */
HMap.PAN_DIRECTION_FACTOR = 0.66;

HMap.NAVIGATION_KEYCODE_LEFT       = 37;   // left arrow
HMap.NAVIGATION_KEYCODE_UP         = 38;   // up arrow
HMap.NAVIGATION_KEYCODE_RIGHT      = 39;   // right arrow
HMap.NAVIGATION_KEYCODE_DOWN       = 40;   // down arrow
HMap.NAVIGATION_KEYCODE_ZOOM_IN    = 34; // page down  107;  // + //
HMap.NAVIGATION_KEYCODE_ZOOM_OUT   = 33; // page up  109;  // - //

/**
 * Interval in milliseconds two consecutive right clicks must be done
 * to be interpreted as a double click.
 * @constant
 * @private
 */
HMap.RIGHT_DOUBLE_CLICK_INTERVAL = 500;

/**
 * Event triggered when dragging of map starts.
 * @event
 */
HMap.EVENT_MAP_DRAG_START = "mapdragstart";

/**
 * Event triggered when dragging of map ends.
 * @event
 */
HMap.EVENT_MAP_DRAG_END = "mapdragend";

/**
 * Event triggered when moving or repositioning of map is done.
 * @event
 */
HMap.EVENT_MAP_MOVE_END = "mapmoveend";

/**
 * Event triggered, possibly repeatedly, when map is moving.
 * @event
 */
HMap.EVENT_MAP_MOVE = "mapmove";

/**
 * Internal event triggered, possibly repeatedly, when map is moving.
 * @event
 * @private
 */
HMap.EVENT_INTERNAL_MAP_MOVE = "internalmapmove";

/**
 * Internal event triggered when map pan causes tile to flip.
 * @event
 * @private
 */
HMap.EVENT_INTERNAL_TILE_FLIP = "internaltileflip";

/**
 * Event triggered when mouse is moved within map.
 * 
 * @event
 * @param {HPointRT90} rt90point Mouse coordinate in RT90.
 * @param {HPoint} containerPos Mouse coordinate in pixels.
 */
HMap.EVENT_MAP_MOUSE_MOVE = "mapmousemove";

/**
 * Event triggered when map is zoomed.
 *
 * @event
 * @param {Number} oldZoomLevel Old zoom index.
 * @param {Number} newZoomLevel New zoom index.
 */
HMap.EVENT_MAP_ZOOM = "mapzoom";

/**
 * Internal event triggered when map is zoomed.
 *
 * @event
 * @param {Number} oldZoomLevel Old zoom index.
 * @param {Number} newZoomLevel New zoom index.
 * @private
 */
HMap.EVENT_INTERNAL_MAP_ZOOM = "internalmapzoom";

/**
 * Event triggered when map is clicked.
 *
 * @event
 * @param {HPointRT90} rt90point Object representing RT90Point coordinates of click on map.
 */
HMap.EVENT_MAP_CLICK = "mapclick";

/**
 * Event triggered when an HMarker is clicked.
 * 
 * @event
 * @param {HMarker} marker The marker that is clicked.
 */
HMap.EVENT_MARKER_CLICK = "mapmarkerclick";

/**
 * Event triggered when an HMarker is double clicked.
 *
 * @event
 * @param {HMarker} marker The marker that is double clicked.
 */
HMap.EVENT_MARKER_DBLCLICK = "mapmarkerdblclick";

/**
 * Event triggered when map is double clicked using the left mouse button.
 *
 * @event
 * @param {HPointRT90} rt90point Object representing RT90Point coordinates of double clicked position on map.
 */
HMap.EVENT_MAP_DBLCLICK = "mapdblclick";

/**
 * Event triggered when map is double clicked using the right mouse button.
 *
 * @event
 * @param {HPointRT90} rt90point Object representing RT90Point coordinates of double clicked position on map.
 */
HMap.EVENT_MAP_DBLCLICK_RIGHT = "mapdblclickright";

/**
 * Event triggered when map type has changed.
 *
 * @event
 * @param {Number} oldMapType Old map type index.
 * @param {Number} newMapType New map type index.
 */
HMap.EVENT_MAP_TYPE_CHANGED = "maptypechanged";

/**
 * Internal event triggered when map type has changed.
 *
 * @event
 * @param {Number} oldMapType Old map type index.
 * @param {Number} newMapType New map type index.
 * @private
 */
HMap.EVENT_INTERNAL_MAP_TYPE_CHANGED = "internalmaptypechanged";

/**
 * Event triggered when overlay is added to the map.
 *
 * @event
 * @param {HOverlay} overlay The added overlay object.
 */
HMap.EVENT_OVERLAY_ADDED = "addoverlay";

/**
 * Event triggered just before overlay is removed from the map (in order to
 * supply overlay object as parameter).
 *
 * @event
 * @param {HOverlay} overlay The removed overlay object.
 */
HMap.EVENT_OVERLAY_REMOVED = "removeoverlay";

/**
 * Event triggered when overlays are cleared.
 *
 * @event
 * @param {String} category Category is only supplied if clearing is made by category.
 * @param {Boolean} exceptArg If true all categories are removed EXCEPT the one specified.
 */
HMap.EVENT_OVERLAYS_CLEARED = "clearoverlays";

/**
 * Event triggered when an info box is opened.
 *
 * @event
 * @param {HInfoBox} infobox The opened info box.
 */
HMap.EVENT_INFO_BOX_OPEN = "infoboxopen";

/**
 * Event triggered when an info box is closed.
 *
 * @event
 * @param {HInfoBox} infobox The closed info box.
 */
HMap.EVENT_INFO_BOX_CLOSE = "infoboxclose";

/**
 * Event triggered when map provider status is updated.
 * Source: HStandardMapProvider
 *
 * @event
 * @param {Object} status Status settings object (typically zoom-levels).
 */
HMap.EVENT_MAP_STATUS_UPDATE = "mapstatusupdate";

/**
 * Internal event triggered when map provider status is updated.
 * Source: HStandardMapProvider
 *
 * @event
 * @param {Object} status Status settings object (typically zoom-levels).
 * @private
 */
HMap.EVENT_INTERNAL_MAP_STATUS_UPDATE = "mapstatusupdate";

/**
 * Initialize globals.
 * @private
 */
HMap.prototype.initGlobals = function() {
    var G = this.globals;
    
    if (this.map.offsetWidth == 0 || this.map.offsetHeight == 0) {
        // temp solution to solve init error on DetailPink/White where mapContainer is width/height 0x0
        G.mapSize = new HSize(598, 602);
    } else {
        G.mapSize = new HSize(this.map.offsetWidth, this.map.offsetHeight);
    }

    G.mapPosition = HMapHelpers.getElementPosition(this.map);

    G.panDestination = new HPoint(0, 0);
    G.panCurrent = new HPoint(0, 0);

    G.tilesPerRow = Math.ceil(G.mapSize.width / HTile.TILE_WIDTH) + HMap.TILE_PADDING;
    G.tilesPerColumn = Math.ceil(G.mapSize.height / HTile.TILE_HEIGHT) + HMap.TILE_PADDING;
    
    G.mapPaddingX = Math.ceil(((G.tilesPerRow * HTile.TILE_WIDTH) - G.mapSize.width) / 2);
    G.mapPaddingY = Math.ceil(((G.tilesPerColumn * HTile.TILE_HEIGHT) - G.mapSize.height) / 2);

    G.tileCount = G.tilesPerRow * G.tilesPerColumn;

    // add controls upon first initialization
    if (!G.isInitialized) {
        this.addControl(new HCopyrightControl());
    }

    G.isInitialized = true;
};

/**
 * Create grid of tiles.
 * @private
 */
HMap.prototype.initGrid = function() {
    var G = this.globals;
    
    HMapHelpers.clearElement(G.gridContainer);
    this.getMapProviderManager().initTiles();

    this.grid = [];
        
    for (var i = 0; i < G.tileCount; i++) {
        var tmpTile = new HTile(this, i);
        this.grid.push(tmpTile);
    }
};

/**
 * Returns number of tiles the current map consists of.
 * 
 * @returns {Number} Number of tiles.
 * @private
 */
HMap.prototype.getTileCount = function() {
    return this.globals.tileCount;
};

/**
 * Enable dragging of the map (enabled by default).
 */
HMap.prototype.enableDragging = function() {
    this.globals.draggingEnabled = true;
};

/**
 * Disables dragging of the map.
 */
HMap.prototype.disableDragging = function() {
    this.globals.draggingEnabled = false;
};

/**
 * Returns true if dragging of the map is enabled.
 *
 * @returns {Boolean} True if dragging is enabled.
 */
HMap.prototype.isDraggingEnabled = function() {
    return this.globals.draggingEnabled;
};

/**
 * Enable double click zoom (enabled by default).
 */
HMap.prototype.enableDoubleClickZoom = function() {
    this.globals.doubleClickZoomEnabled = true;
};

/**
 * Disable double click zoom.
 */
HMap.prototype.disableDoubleClickZoom = function() {
    this.globals.doubleClickZoomEnabled = false;
};

/**
 * Returns true if double click zoom is enabled.
 *
 * @returns {Boolean} True if double click zoom is enabled.
 */
HMap.prototype.isDoubleClickZoomEnabled = function() {
    return this.globals.doubleClickZoomEnabled;
};

/**
 * Enable mouse wheel zoom (enabled by default).
 */
HMap.prototype.enableMouseWheelZoom = function() {
    this.globals.mouseWheelZoomEnabled = true;
};

/**
 * Disable mouse wheel zoom.
 */
HMap.prototype.disableMouseWheelZoom = function() {
    this.globals.mouseWheelZoomEnabled = false;
};

/**
 * Returns true if mouse wheel zoom is enabled.
 *
 * @returns {Boolean} True if double click zoom is enabled.
 */
HMap.prototype.isMouseWheelZoomEnabled = function() {
    return this.globals.mouseWheelZoomEnabled;
};

/**
 * Enable exclusive infobox mode (enabled by default). When enabled
 * only one infobox attached to a marker may be displayed at once.
 */
HMap.prototype.enableExclusiveInfoBox = function() {
    this.globals.exclusiveInfoBoxEnabled = true;
};

/**
 * Disable exclusive infobox mode.
 */
HMap.prototype.disableExclusiveInfoBox = function() {
    this.globals.exclusiveInfoBoxEnabled = false;
};

/**
 * Returns true if exclusive infobox mode is enabled.
 *
 * @returns {Boolean} True if exclusive infobox mode is enabled.
 */
HMap.prototype.isExclusiveInfoBoxEnabled = function() {
    return this.globals.exclusiveInfoBoxEnabled;
};

/**
 * Get container element that contains the whole HMap component.
 *
 * @return {Element} Map component parent element.
 */
HMap.prototype.getContainer = function() {
    return this.map;
};

/**
 * Get DIV element that contains the map.
 *
 * @return {Element} Element that contains map.
 */
HMap.prototype.getPaneMap = function() {
    return this.globals.gridContainer;
};

/**
 * Get DIV element that contains the overlays. Note that this
 * element may not exclusively just contain overlays.
 *
 * @return {Element} Element that contains the overlays.
 */
HMap.prototype.getPaneOverlay = function() {
    return this.globals.overlayContainer;
};

/**
 * Get DIV element that contains the controls. Note that this
 * element may not exclusively just contain controls.
 *
 * @return {Element} Element that contains the controls.
 */
HMap.prototype.getPaneControls = function() {
    return this.globals.overlayContainer;
};

/**
 * Get map provider manager instance that manages the different
 * map providers, making it possible to add and remove map layers.
 *
 * @return {HMapProviderManager} Map Provider Manager instance.
 */
HMap.prototype.getMapProviderManager = function() {
    return this.mapProviderManager;
};

/**
 * Get map polygon layer object. Will initialize it if needed.
 * 
 * @return {HPolyLayer} Map's instance of polygon layer object.
 */
HMap.prototype.getPolyLayer = function() {
    var G = this.globals;
    
    if (!G.polyLayer.isInitialized()) {
        G.polyLayer.initialize();
    }

    return G.polyLayer;
};

/**
 * Add overlay object to map. Object must implement HOverlay.
 *
 * An overlay may be assigned a category to make it easy to remove categories
 * of overlay objects without the need to keep references to every single
 * object.
 *
 * If explicit is set to true the overlay may only be explicitly deleted using
 * either removeOverlay() or clearOverlaysByCategory() where the object's
 * category is the argument. Using clearOverlays() will not remove the overlay.
 * 
 * @param {Object} overlay Overlay object to add to map.
 * @param {String} [category] Category to assign overlay to.
 * @param {Boolean} [explicit] If overlay may only be deleted explicitly.
 */
HMap.prototype.addOverlay = function(overlay, category, explicit) {
    // set category to overlay
    overlay.category = arguments[1] || '';
    overlay.categoryExplicit = arguments[2] || false;
    
    // add to overlay array
    this.overlays.push(overlay);
    
    overlay.isAdded = true;    
    overlay.init(this);
    
    HEvent.trigger(this, HMap.EVENT_OVERLAY_ADDED, overlay);
};

/**
 * Get number of overlay elements added to the map.
 *
 * @returns {Number} Number of overlay elements added to the map.
 */
HMap.prototype.getOverlayCount = function() {
    return this.overlays.length;
};

/**
 * Remove overlay object from map.
 *
 * @param {Object} overlay Overlay object to remove from map.
 */
HMap.prototype.removeOverlay = function(overlay) {
    HEvent.trigger(this, HMap.EVENT_OVERLAY_REMOVED, overlay);

    overlay.isAdded = false;
    
    // call remove method of overlay
    overlay.remove();

    // remove from overlays array
    this.overlays.remove(overlay);
};

/**
 * Removes all overlays.
 *
 * @param {Boolean} [explicit] Set to true to force removal of explicit
 *                             categories. Default is false.
 */
HMap.prototype.clearOverlays = function(explicit) {
    var explicitArg = arguments[0] || false;

    // remove from container and call remove method of overlay object
    for (var i = this.overlays.length - 1; i > -1 ; i--) {
        if (explicitArg || !this.overlays[i].categoryExplicit) {
            this.overlays[i].isAdded = false;
            this.overlays[i].remove();
            this.overlays.splice(i, 1);
        }
    }
    
    // delete overlays array and reinitiate it
    if (this.overlays.length == 0) {
        delete this.overlays;
        this.overlays = [];
    }
    
    HEvent.trigger(this, HMap.EVENT_OVERLAYS_CLEARED);
};

/**
 * Remove overlays by category.
 * 
 * @param {String} category Overlay category to remove.
 * @param {Boolean} [except] If true all categories are removed EXCEPT the
 *                           one given.
 */
HMap.prototype.clearOverlaysByCategory = function(category, except) {
    var exceptArg = arguments[1] || false;
    
    for (var i = this.overlays.length - 1; i > -1 ; i--) {
        if ((!exceptArg && this.overlays[i].category === category) ||
            (exceptArg && this.overlays[i].category !== category && !this.overlays[i].categoryExplicit)) {
            this.overlays[i].isAdded = false;
            
            // call remove method of overlay
            this.overlays[i].remove();
            
            // remove from overlays array
            this.overlays.splice(i, 1);
        }
    }
    
    HEvent.trigger(this, HMap.EVENT_OVERLAYS_CLEARED, category, exceptArg);
};

/**
 * Update overlays.
 * @private
 */
HMap.prototype.updateOverlays = function() {
    for (var i = 0; i < this.overlays.length; i++) {
        if (this.overlays[i].isActive) {
            this.overlays[i].update();
        }
    }
};

/**
 * Get all markers currently added to the map. If bounds parameter is set
 * only those markers within that bound are returned.
 *
 * @param {HBoundsRT90} [bounds] Only markers contained in bound are returned.
 * @return {Array} Array of HMarker.
 */
HMap.prototype.getMarkers = function() {
    var bounds = arguments[0] || false;

    var result = [];
    for (var i = 0; i < this.overlays.length; i++) {
        if (this.overlays[i].isMarker) {
            if (!bounds || bounds.containsRT90(this.overlays[i].getRT90Point())) {
                result.push(this.overlays[i]);
            }
        }
    }
    
    return result;
};

/**
 * Update poly layer.
 * @private
 */
HMap.prototype.updatePolyLayer = function() {
    var G = this.globals;

    if (G.polyLayer.isInitialized()) {
        G.polyLayer.update();
    }
};

/**
 * Add control to map.
 *
 * @param {HControl} control Control to add.
 * @param {HControlPosition} [position] Optional position, or else control's
 *                           default position will be used.
 */
HMap.prototype.addControl = function(control, position) {
    var pos = arguments[1] || false;
    if (!pos) {
        pos = control.getDefaultPosition();
    }

    // add to controls array
    this.controls.push(control);

    control.init(this, pos);
};

/**
 * Remove control from map.
 *
 * @param {HControl} control Control to remove.
 */
HMap.prototype.removeControl = function(control) {
    // call remove method of control
    control.remove();

    // remove from controls array
    this.controls.remove(control);
};

/**
 * Update controls.
 * @private
 */
HMap.prototype.updateControls = function() {
    for (var i = 0; i < this.controls.length; i++) {
        this.controls[i].update();
    }
};

/**
 * Update map position.
 * @private
 */
HMap.prototype.initPosition = function() {
    // update tiles
    for (var i = 0; i < this.grid.length; i++) {
        this.grid[i].initPosition();
    }
    
    this.updateOverlays();
    this.updatePolyLayer();
    this.updateMapTiles();
    
    HEvent.trigger(this, HMap.EVENT_MAP_MOVE_END);
    this.updateStatus();
};

/**
 * Update map tiles.
 * @private
 */
HMap.prototype.updateMapTiles = function() {
    for (var i = 0; i < this.grid.length; i++) {
        this.grid[i].updateMapTile();
    }
};

/**
 * Trigger update of status for currently added map providers (gets available zoom levels, etc).
 * @private
 */
HMap.prototype.updateStatus = function() {
    this.mapProviderManager.updateStatus();
};

/**
 * Set focus in order for map to receive keyboard input. Called by mouseDown()
 * @private
 */
HMap.prototype.setFocus = function() {
    var G = this.globals;
    
    if (HMapHelpers.isSafari() && G.hiddenInputElement != null) {
        // note: a div object may not receive keyboard events in Safari
        // (as of 3.2), therefore a hidden input element is used to
        // listen to keyboard events in Safari.
        G.hiddenInputElement.focus();
    } else if (!HMapHelpers.isIE()) {
        this.map.focus();
    }
};

/**
 * Start drag operation.
 *
 * @param {Object} point Current mouse coordinates.
 * @private
 */
HMap.prototype.startDrag = function(point) {
    var G = this.globals;

    if (G.draggingEnabled) {
        G.isDragging = true;
        G.dragStartEventSent = false;
        G.startX = point.x;
        G.startY = point.y;
        G.deltaX = 0;
        G.deltaY = 0;
    }
};

/**
 * Perform drag operation.
 *
 * @param {Object} point Current mouse coordinates.
 * @private
 */
HMap.prototype.drag = function(point) {
    var G = this.globals;
    
    // calculate delta movement
    var dx = point.x - G.startX;
    var dy = point.y - G.startY;
    
    G.deltaX += dx;
    G.deltaY += dy;
    
    this.pan(dx, dy);
    
    // update latest position
    G.startX = point.x;
    G.startY = point.y;
};

/**
 * Stop drag operation.
 *
 * @param {Object} point Current mouse coordinates.
 * @private
 */
HMap.prototype.stopDrag = function(point) {
    var G = this.globals;

    if (G.isDragging) {
        G.isDragging = false;
        G.startX = point.x;
        G.startY = point.y;
        
        if (G.dragStartEventSent) {
            HEvent.trigger(this, HMap.EVENT_MAP_DRAG_END);
            HEvent.trigger(this, HMap.EVENT_MAP_MOVE_END);
            
            this.updateStatus();
        }
    }
};

/**
 * Mouse move event handler that triggers custom mouse move event. Listens
 * to map container.
 *
 * @param {Object} event Event.
 * @private
 */
HMap.prototype.mouseMoveMap = function(event) {
    var G = this.globals;
    var pos = HMapHelpers.getMousePosition(event);
    G.currentMousePos = pos;

    if (HEvent.isListening(this, HMap.EVENT_MAP_MOUSE_MOVE)) {
        var containerPos = new HPoint(pos.x - G.mapPosition.x, pos.y - G.mapPosition.y);
        var rt90point = this.fromContainerPixelToRT90(containerPos);

        HEvent.trigger(this, HMap.EVENT_MAP_MOUSE_MOVE, rt90point, containerPos);
    }
};

/**
 * Mouse down event handler initiates dragging and sets focus to map.
 *
 * @param {Object} event Event.
 * @private
 */
HMap.prototype.mouseDown = function(event) {
    var e = event || window.event;
    var pos = HMapHelpers.getMousePosition(e);
    
    // set focus to enable to receive keyboard events
    this.setFocus();
    
    if (HMapHelpers.isLeftMouseClicked(e)) {
        this.startDrag(pos);

        // stop event to prevent browser drag-and-drop for images to kick in
        HEvent.stopEvent(e);
    }
};

/**
 * Mouse move event handler pans grid when dragging. Listens to document.
 *
 * @param {Object} event Event.
 * @private
 */
HMap.prototype.mouseMove = function(event) {
    var e = event || window.event;
    var G = this.globals;
    
    if (G.isDragging) {
        var pos = HMapHelpers.getMousePosition(e);
        
        if (!G.dragStartEventSent) {
            HEvent.trigger(this, HMap.EVENT_MAP_DRAG_START);
            G.dragStartEventSent = true;
        }
        
        if (HMapHelpers.isIEEvent(e) && !HMapHelpers.isLeftMouseClicked(e)) {
            // handle special case in IE when mouse is dragged outside browser
            // window and mouse button is released.
            this.stopDrag(pos);
        } else {
            this.drag(pos);
        }
    }
};

/**
 * Mouse up event handler detects when dragging has stopped and if right double
 * click is performed. Listens to document.
 *
 * @param {Object} event Event.
 * @private
 */
HMap.prototype.mouseUp = function(event) {
    var e = event || window.event;
    var G = this.globals;
    var pos = HMapHelpers.getMousePosition(e);

    if (HMapHelpers.isLeftMouseClicked(e)) {
        this.stopDrag(pos);
    }
    
    if (HMapHelpers.isRightMouseClicked(e)) {
        var now = new Date().getTime();

        // right-double click performed
        if (now - G.rightClickTimestamp < HMap.RIGHT_DOUBLE_CLICK_INTERVAL) {
            // save coordinate where clicked
            var rt90point = this.fromContainerPixelToRT90(
                new HPoint(pos.x - G.mapPosition.x, pos.y - G.mapPosition.y)
            );

            G.rightClickTimestamp = 0;

            HEvent.trigger(this, HMap.EVENT_MAP_DBLCLICK_RIGHT, rt90point);
            
            // zoom out (by center, feels disorientated otherwise)
            if (G.doubleClickZoomEnabled) {
                this.zoomOut();
            }
        }
        
        G.rightClickTimestamp = now;
    }
};

/**
 * Mouse click event handler.
 *
 * @param {Object} event Event.
 * @private
 */
HMap.prototype.mouseClick = function(event) {
    var e = event || window.event;
    var G = this.globals;

    var pos = HMapHelpers.getMousePosition(e);

    // skip if negative coordinates (seems to happen on links in
    //   infoboxes causing weird event coordinates)
    // skip if dragged
    if ((pos.x < 0 || pos.y < 0) ||                 
        (G.deltaX !== 0 || G.deltaY !== 0)) {       
        return;
    }

    // save coordinate where clicked
    var rt90Point = this.fromContainerPixelToRT90(
        new HPoint(pos.x - G.mapPosition.x, pos.y - G.mapPosition.y)
    );
    
    HEvent.trigger(this, HMap.EVENT_MAP_CLICK, rt90Point);
};

/**
 * Mouse double click event handler.
 *
 * @param {Object} event Event.
 * @private
 */
HMap.prototype.mouseDblClick = function(event) {
    var e = event || window.event;
    var G = this.globals;
    var pos = HMapHelpers.getMousePosition(e);

    // save coordinate where clicked
    var rt90point = this.fromContainerPixelToRT90(
        new HPoint(pos.x - G.mapPosition.x, pos.y - G.mapPosition.y)
    );
    
    HEvent.trigger(this, HMap.EVENT_MAP_DBLCLICK, rt90point);
    
    // zoom in
    if (G.doubleClickZoomEnabled) {
        this.setZoom(this.getZoom() + 1, rt90point);
    }
};

/**
 * Mouse scroll wheel event handler that manages zooming using the mouse wheel.
 * Mouse wheel zoom maintains the position under the mouse pointer, to allow
 * continous zoom by scrolling the wheel without moving the mouse pointer.
 *
 * @param {Object} event Event.
 * @returns {Boolean} If mouse wheel zoom enabled, returns false to cancel
 *                    bubbling. Otherwise returns true.
 * @private
 */
HMap.prototype.mouseWheel = function(event) {
    var G = this.globals;

    if (G.mouseWheelZoomEnabled && G.currentMousePos !== null) {
        var e = event || window.event;

        var wheelData = e.detail ? e.detail * -1 : e.wheelDelta / 40;

        // Note: due to a bug in Firefox 2 pixel coordinates in DOMMouseWheel
        // event is weird, therefore current mouse position is fetched from
        // HMap.global variable updated by mousemove event.
        //   https://bugzilla.mozilla.org/show_bug.cgi?id=352179
        var mousePos = G.currentMousePos;

        var pos = new HPoint(mousePos.x - G.mapPosition.x, mousePos.y - G.mapPosition.y);

        // save coordinate where clicked
        var rt90point = this.getCenter();

        // zoom in or out?
        var newLevel;
        if (wheelData > 0) {
            newLevel = this.getZoom() + 1;
        } else {
            newLevel = this.getZoom() - 1;
        }

        if (this.isZoomLevelValid(newLevel)) {
            // diff in pixels
            var diffX = Math.round(G.mapSize.width / 2) - pos.x;
            var diffY = Math.round(G.mapSize.height / 2) - pos.y;

            // get resolution of new zoom level
            var res = HMap.ZOOM_LEVELS[newLevel];

            // offset to maintain position at mouse pointer
            rt90point.east -= (G.resolution - res) * diffX;
            rt90point.north += (G.resolution - res) * diffY;

            this.setZoom(newLevel, rt90point);
        }

        return HEvent.stopEvent(event);
    }

    return true;
};

/**
 * Key  event handler. Depending on browser this may triggered on keyDown
 * or keyPress event.
 *
 * Note: For map container div to be able to receive key events the div
 * must have tabIndex set to 0 and have focus. Focus is programmatically set
 * in the mouseDown() event handler.
 *
 * @param {Object} event Event.
 * @private
 */
HMap.prototype.keyAction = function(event) {
    var e = event || window.event;
    var code = e.charCode || e.keyCode;

    var handled = true;
    
    switch (code) {
        case HMap.NAVIGATION_KEYCODE_LEFT:
            this.panDirection(+1, 0);
            break;
        case HMap.NAVIGATION_KEYCODE_UP:
            this.panDirection(0, +1);
            break;
        case HMap.NAVIGATION_KEYCODE_RIGHT:
            this.panDirection(-1, 0);
            break;
        case HMap.NAVIGATION_KEYCODE_DOWN:
            this.panDirection(0, -1);
            break;
        case HMap.NAVIGATION_KEYCODE_ZOOM_IN:
            this.zoomIn();
            break;
        case HMap.NAVIGATION_KEYCODE_ZOOM_OUT:
            this.zoomOut();
            break;
        default:
            handled = false;
    }
    
    // if handled, prevent keypress from bubbling to browser
    if (handled) {
        return HEvent.stopEvent(e);
    }

    return true;
};

/**
 * Map checks of a change in size of its container and updates itself
 * accordingly. Call when onResize is triggered to get a dynamic size of the
 * map container relative to width/height of browser window.
 */
HMap.prototype.checkResize = function() {
    var G = this.globals;

    // get current center of map view
    var currentCenter = this.getCenter();

    // reinit grid of tiles
    this.initGlobals();
    this.initGrid();

    this.updateOverlays();
    this.updatePolyLayer();
    this.updateControls();

    // if initialized, then re-initialize
    if (G.polyLayer.isInitialized()) {
        G.polyLayer.initialize();
    }

    // recenter map        
    this.setCenter(currentCenter);
};

/**
 * Pan map. This moves the map the given distance in one step. Use panBy()
 * for gradual panning using animation.
 *
 * @param {Number} dx Delta X movement in pixels to pan.
 * @param {Number} dy Delta Y movement in pixels to pan.
 */
HMap.prototype.pan = function(dx, dy) {
    var G = this.globals;

    var i;

    // pan tiles
    var flipped = false;
    for (i = 0; i < this.grid.length; i++) {
        if (this.grid[i].pan(dx, dy)) {
            flipped = true;
        }
    }
    
    // pan points
    for (i = 0; i < this.overlays.length; i++) {
        if (this.overlays[i].isActive) {
            this.overlays[i].pan(dx, dy);
        }
    }
    
    // poly layer
    if (G.polyLayer.isInitialized()) {
        G.polyLayer.pan(dx, dy);
    }

    // invalidate via API programmatically set center point
    G.centerPoint = null;

    HEvent.trigger(this, HMap.EVENT_INTERNAL_MAP_MOVE);
    HEvent.trigger(this, HMap.EVENT_MAP_MOVE);
    
    if (flipped) {
        HEvent.trigger(this, HMap.EVENT_INTERNAL_TILE_FLIP);
    }
};

/**
 * Pan map by animation. This moves the map the given distance animated in
 * several steps.
 *
 * @param {Number} dx Delta X movement in pixels to pan.
 * @param {Number} dy Delta Y movement in pixels to pan.
 */
HMap.prototype.panBy = function(dx, dy) {
    var G = this.globals;
    var me = this;

    if (!me.panTimer) {
        G.panDestination.x = dx;
        G.panDestination.y = dy;

        G.panCurrent.x = 0;
        G.panCurrent.y = 0;

        me.panTimer = setInterval(function() {
            var sx = (G.panDestination.x - G.panCurrent.x) / HMap.PAN_ANIMATION_SPEED;  // stepX
            var sy = (G.panDestination.y - G.panCurrent.y) / HMap.PAN_ANIMATION_SPEED;

            sx = (sx < 0) ? Math.floor(sx) : Math.ceil(sx);
            sy = (sy < 0) ? Math.floor(sy) : Math.ceil(sy);

            // restrict max step to 1/4 of map size
            sx = HMapHelpers.signMin(sx, Math.ceil(G.mapSize.width / HMap.PAN_MAX_STEP_SIZE_PART));
            sy = HMapHelpers.signMin(sy, Math.ceil(G.mapSize.height / HMap.PAN_MAX_STEP_SIZE_PART));

            me.pan(sx, sy);
            G.panCurrent.x += sx
            G.panCurrent.y += sy;

            if (G.panCurrent.x == G.panDestination.x && G.panCurrent.y == G.panDestination.y) {
                clearInterval(me.panTimer);
                me.panTimer = null;

                me.updateStatus();
                HEvent.trigger(me, HMap.EVENT_MAP_MOVE_END);
            }
        }, 10);
    } else {
        // if animation timer is currenty panning, modify destination
        // instead of starting a new timer, however, restrict how much
        // is added to prevent the map from continue panning too long
        // after user releases key
        if (Math.abs(G.panDestination.x - G.panCurrent.x) < G.mapSize.width &&
            Math.abs(G.panDestination.y - G.panCurrent.y) < G.mapSize.height) {
                G.panDestination.x += dx;
                G.panDestination.y += dy;
        }
    }
};

/**
 * Pan map in given direction about 2/3 of the map size.
 *
 * @param {Number} dirHorz Set to -1 to move left, +1 for right and 0 for no
 *                         horizontal movement.
 * @param {Number} dirVert Set to -1 to move up, +1 for down and 0 for no
 *                         vertical movement.
 */
HMap.prototype.panDirection = function(dirHorz, dirVert) {
    var G = this.globals;
    
    var panHorz = Math.ceil(dirHorz * (G.mapSize.width - G.paddingLeft - G.paddingRight) * HMap.PAN_DIRECTION_FACTOR);
    var panVert = Math.ceil(dirVert * (G.mapSize.height - G.paddingTop - G.paddingBottom) * HMap.PAN_DIRECTION_FACTOR);
    
    this.panBy(panHorz, panVert);
};

/**
 * Pan map to given point. If point is within view gradual animated panning
 * is used, otherwise the map is recentered.
 *
 * @param {HPointRT90} rt90point Point in RT90 to move to.
 */
HMap.prototype.panTo = function(rt90point) {
    var G = this.globals;

    // get visible bounds
    var bounds = this.getBounds();

    // enlarge to perform animated panning even if slightly outside visible area
    bounds.scale(HMap.PAN_TO_SMOOTH_SCROLL_PADDING);

    if (bounds.containsRT90(rt90point)) {
        // pan with pixels
        var current = new HPoint(Math.round(G.mapSize.width / 2), Math.round(G.mapSize.height / 2));
        var point = this.fromRT90toContainerPixel(rt90point);

        this.panBy(current.x - point.x, current.y - point.y);
    } else {
        // set new center
        this.setCenter(rt90point);
    }
};

/**
 * Get size in pixels of map view.
 *
 * @returns {HSize} Size of map view.
 */
HMap.prototype.getSize = function() {
    return this.globals.mapSize;
};

/**
 * Get bounds of current visual area as geographical RT90 coordinates.
 *
 * @returns {HBoundsRT90} Bounds of current view.
 */
HMap.prototype.getBounds = function() {
   var G = this.globals;

   var sw = this.fromContainerPixelToRT90(new HPoint(G.paddingLeft, G.mapSize.height - G.paddingBottom));
   var ne = this.fromContainerPixelToRT90(new HPoint(G.mapSize.width - G.paddingRight, G.paddingTop));

   return new HBoundsRT90(sw, ne);
};

/**
 * Set visible padding area of map container. This is useful when having
 * elements overlapping the map container making it only partially visible.
 * Controls, markers and bounds does consider padding when considering the
 * visible area of the map container.
 *
 * @param {Number} top Padding of top edge.
 * @param {Number} right Padding of right edge.
 * @param {Number} bottom Padding of bottom edge.
 * @param {Number} left Padding of left edge.
 */
HMap.prototype.setVisiblePadding = function(top, right, bottom, left) {
    var G = this.globals;

    G.paddingTop = top;
    G.paddingRight = right;
    G.paddingBottom = bottom;
    G.paddingLeft = left;

    if (G.isInitialized) {
        this.updateControls();
    }
};

/**
 * Get the maximum zoom level where the given bounds fit in the map view. The
 * returned value does not consider wheter it is a valid zoom level the current
 * view.
 * 
 * @param {HBoundsRT90} bounds Rectangular region.
 * @param {HSize} [size] Map size to get bounds for, uses current map size if not set.
 * @returns {Number} Zoom level.
 */
HMap.prototype.getBoundsZoomLevel = function(bounds, size) {
    var G = this.globals;
    var mapSize = arguments[1] || G.mapSize;
    var boundsSpan = bounds.toSpan();

    for (var i = HMap.ZOOM_LEVELS.length - 1; i > 0; i--) {
        if ((mapSize.width - G.paddingLeft - G.paddingRight) * HMap.ZOOM_LEVELS[i] > boundsSpan.east &&
            (mapSize.height - G.paddingTop - G.paddingBottom) * HMap.ZOOM_LEVELS[i] > boundsSpan.north) {
                return i;
        }
    }

    return 0;
};

/**
 * Closes any open infoboxes attached to a marker.
 */
HMap.prototype.closeInfoBoxes = function() {
    for (var i = 0; i < this.overlays.length; i++) {
        if (this.overlays[i].infoBox) {
            this.overlays[i].closeInfoBox();
        }
    }
};

/**
 * Get distance in meters between two points given in container pixel
 * coordinates of current zoom level.
 *
 * @param {HPoint} point1
 * @param {HPoint} point2
 *
 * @returns {Number} Distance in meters.
 */
HMap.prototype.getDistancePixel = function(point1, point2) {
    var G = this.globals;

    return G.resolution * Math.sqrt( Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2) );
};

/**
 * Change zoom level if level is valid. If level
 * is not valid false is returned.
 *
 * @param {Number} toZoomLevel Zoom to this level.
 * @param {HPointRT90} [rt90point] Position to center map at after zoom (default
 *                                 is center position of map before zoom)
 * 
 * @returns {Boolean} True if successfull
 */
HMap.prototype.setZoom = function(toZoomLevel, rt90point) {
    if (!this.isZoomLevelValid(toZoomLevel)) {
        return false;
    }

    // use argument if given, else current map center
    var centerRT90point = arguments[1] || this.getCenter();

    var G = this.globals;
    var oldZoomLevel = G.zoomLevel;

    this.setCenter(centerRT90point, toZoomLevel);

    if (oldZoomLevel != G.zoomLevel) {
        HEvent.trigger(this, HMap.EVENT_INTERNAL_MAP_ZOOM, oldZoomLevel, G.zoomLevel);
        HEvent.trigger(this, HMap.EVENT_MAP_ZOOM, oldZoomLevel, G.zoomLevel);
    }

    return true;
};

/**
 * Zoom in (by center position of map).
 */
HMap.prototype.zoomIn = function() {
    this.setZoom(this.getZoom() + 1);
};

/**
 * Zoom out (by center position of map).
 */
HMap.prototype.zoomOut = function() {
    this.setZoom(this.getZoom() - 1);
};

/**
 * Zoom to maximum zoom level of current view and map type.
 */
HMap.prototype.zoomMax = function() {
    this.setZoom(this.mapProviderManager.getBaseMap().getMaxZoomLevel());
};

/**
 * Get current zoom level.
 * 
 * @returns {Number} Current zoom level.
 */
HMap.prototype.getZoom = function() {
    var G = this.globals;
    
    return G.zoomLevel;
};

/**
 * Returns resolution based on current zoom level. Resolution is described
 * as meters per pixel.
 * 
 * @returns {Number} Current resolution.
 */
HMap.prototype.getResolution = function() {
    var G = this.globals;

    return HMap.ZOOM_LEVELS[G.zoomLevel];
};

/**
 * Checks if a zoom level is valid.
 *
 * @param {Number} zoomLevel The zoom level to check
 * @param {Number} [mapType] Map type to check (uses current mapType if left out)
 */
HMap.prototype.isZoomLevelValid = function(zoomLevel, mapType) {
    var mapProvider = this.mapProviderManager.getBaseMap();

    return (zoomLevel >= mapProvider.getMinZoomLevel(mapType) && zoomLevel <= mapProvider.getMaxZoomLevel(mapType));
};

/**
 * Get maximum available zoom level of current position.
 *
 * @param {Number}[mapType] Map type to check (uses current mapType if left out)
 */
HMap.prototype.getMaxZoomLevel = function(mapType) {
    var mapProvider = this.mapProviderManager.getBaseMap();
    
    return mapProvider.getMaxZoomLevel(mapType);
};

/**
 * Set zoom level (validness is assumed, otherwise use setZoom()).
 * 
 * @param {Number} zoomLevel The zoom level to set
 * @private
 */
HMap.prototype.setZoomLevel = function(zoomLevel) {
    var G = this.globals;

    G.zoomLevel = zoomLevel;
    G.resolution = HMap.ZOOM_LEVELS[G.zoomLevel];
};

/**
 * Set map type of current map provider.
 *
 * @param {Number} mapType Map type.
 */
HMap.prototype.setMapType = function(mapType) {
    var mapProvider = this.mapProviderManager.getBaseMap();
    
    var oldMapType = mapProvider.getMapType();

    // change current map type
    mapProvider.setMapType(mapType);
    
    if (!this.isZoomLevelValid(this.getZoom(), mapType)) {
        // if not valid, zoom level does not exist, then revert back to max zoom
        this.setZoom(mapProvider.getMaxZoomLevel(mapType));
    } else {
        this.updateMapTiles();
    }

    if (oldMapType != mapType) {
        HEvent.trigger(this, HMap.EVENT_INTERNAL_MAP_TYPE_CHANGED, oldMapType, mapType);
        HEvent.trigger(this, HMap.EVENT_MAP_TYPE_CHANGED, oldMapType, mapType);
    }
};

/**
 * Get current map type of base map provider.
 *
 * @returns {Number} Map type.
 */
HMap.prototype.getMapType = function() {
    return this.mapProviderManager.getBaseMap().getMapType();
};

/**
 * Set center to given RT90 point and update map. Should be called immediately
 * after a HMap object is instantiated in order to initialize the map. Values
 * of zoom level and map types are assumed to be valid.
 * 
 * @param {HPointRT90} rt90point The RT90 point to set center to.
 * @param {Number} [zoom] Zoom level.
 * @param {Number} [type] Map type.
 */
HMap.prototype.setCenter = function(rt90point, zoom, type) {
    var G = this.globals;

    // get optional arguments
    var zoomLevel = arguments[1] || -1;
    var mapType = arguments[2] || -1;

    // set zoom level
    if (zoomLevel != -1 && (!G.isInitialized || (G.isInitialized && this.isZoomLevelValid(zoomLevel)))) {
        if (G.isInitialized) {
            HEvent.trigger(this, HMap.EVENT_INTERNAL_MAP_ZOOM, this.getZoom(), zoomLevel);
            HEvent.trigger(this, HMap.EVENT_MAP_ZOOM, this.getZoom(), zoomLevel);
        }

        this.setZoomLevel(zoomLevel);
    } else if (this.getZoom() == 0) {
        this.setZoomLevel(HMap.DEFAULT_ZOOM_LEVEL);
    }

    // set map type
    if (mapType != -1) {
        this.mapProviderManager.getBaseMap().setMapType(mapType);
    }

    // initialize if first call
    if (!G.isInitialized) {
        this.initGlobals();
        this.initGrid();
    }

    var res = HMap.ZOOM_LEVELS[G.zoomLevel];
    
    var deltaEast = rt90point.east - HMap.RT90_BOTTOM_LEFT_GEO_POINT_EAST;
    var deltaNorth = rt90point.north - HMap.RT90_BOTTOM_LEFT_GEO_POINT_NORTH;


    var col = Math.ceil( Math.floor(deltaEast / (HTile.TILE_WIDTH * res)) + 0.5 );
    var row = Math.ceil( Math.floor(deltaNorth / (HTile.TILE_HEIGHT * res)) + 0.5 );
    
    var pixelOffsetHorz = (deltaEast % (HTile.TILE_WIDTH * res)) / res;   // offset in pixels from left of tile
    var pixelOffsetVert = (deltaNorth % (HTile.TILE_HEIGHT * res)) / res; // offset in pixels from bottom of tile
    
    // calculate number of tiles under and to the left of the center tile
    var tilesLeft = Math.floor((G.tilesPerRow - 1) / 2);
    var tilesUnder = Math.floor((G.tilesPerColumn - 1) / 2);

    if (G.tilesPerRow % 2 == 0 && Math.round(pixelOffsetHorz) < HTile.TILE_WIDTH / 2)
        tilesLeft += 1;
    if (G.tilesPerColumn % 2 == 0 && Math.round(pixelOffsetVert) < HTile.TILE_HEIGHT / 2)
        tilesUnder += 1;

    G.mapColumn = col - tilesLeft;
    G.mapRow = row - tilesUnder;

    G.tileInitOffsetX = Math.round(G.mapSize.width / 2 - tilesLeft * HTile.TILE_WIDTH - pixelOffsetHorz);
    G.tileInitOffsetY = Math.round(G.mapSize.height / 2 - (G.tilesPerColumn - tilesUnder) * HTile.TILE_HEIGHT + pixelOffsetVert);

    G.centerPoint = rt90point;

    // update map
    this.initPosition();
};

/**
 * Get geographical RT90 coordinates of the center point of the map view.
 *
 * @returns {HPointRT90} Object describing RT90 coordinates (north (X)/east(Y)).
 */
HMap.prototype.getCenter = function() {
    var G = this.globals;

    if (G.centerPoint !== null) {
        return G.centerPoint;
    }

    // get center point in pixels
    var point = new HPoint(Math.round((G.mapSize.width - G.paddingLeft - G.paddingRight) / 2) + G.paddingLeft, Math.round((G.mapSize.height - G.paddingTop - G.paddingBottom) / 2) + G.paddingTop);
    
    return this.fromContainerPixelToRT90(point);
};

/**
 * Converts container pixel coordinates to RT90.
 * 
 * @param {HPoint} point Coordinates of pixel in map container.
 * @returns {HPointRT90} Object describing RT90 coordinates (north (X)/east(Y)).
 */
HMap.prototype.fromContainerPixelToRT90 = function(point /* allowFloats */) {

    // internal second argument, set to true to allow decimal
    // RT90 coordinates to be returned. Default is false;
    var allowFloats = !!arguments[1];

    var G = this.globals;
    var tile = this.grid[0];
    
    var tileDeltaX = Math.floor((point.x - tile.point.x) / HTile.TILE_WIDTH);
    var tileDeltaY = -Math.floor((point.y - tile.point.y) / HTile.TILE_HEIGHT);
    
    var offsetHorz = (point.x - tile.point.x) % HTile.TILE_WIDTH;
    var offsetVert = (point.y - tile.point.y) % HTile.TILE_HEIGHT;
    
    if (offsetHorz < 0) {
        offsetHorz += HTile.TILE_WIDTH;
    }
    if (offsetVert < 0) {
        offsetVert += HTile.TILE_HEIGHT;
    }
    
    var clickMapColumn = tile.mapColumn + tileDeltaX;
    var clickMapRow = tile.mapRow + tileDeltaY;

    var RT90East = HMap.RT90_BOTTOM_LEFT_GEO_POINT_EAST +
                        (((clickMapColumn - 1) * HTile.TILE_WIDTH) + offsetHorz) * G.resolution;
    
    var RT90North = HMap.RT90_BOTTOM_LEFT_GEO_POINT_NORTH +
                        ((clickMapRow * HTile.TILE_HEIGHT) - offsetVert) * G.resolution;

    // rounding necessary for resolution 0.5
    if (!allowFloats && G.resolution < 1) {
        // note: this will result in rounding errors on zoom-level 0.5
        // but will keep RT90 coordinates fixed
        RT90East = Math.round(RT90East);
        RT90North = Math.round(RT90North);
    }

    return new HPointRT90(RT90North, RT90East);
};

/**
 * Converts RT90 coordinates to container pixel position. The returned
 * pixel point may be outside the visible area of the current map view.
 *
 * @param {HPointRT90} rt90point The RT90 point to set center to.
 * @returns {HPoint} Container pixel coordinates.
 */
HMap.prototype.fromRT90toContainerPixel = function(rt90point) {
    var G = this.globals;
    
    var topLeftPoint = this.fromContainerPixelToRT90(new HPoint(0, 0), true);
    
    var deltaEast = Math.round((rt90point.east - topLeftPoint.east) / G.resolution);
    var deltaNorth = -Math.round((rt90point.north - topLeftPoint.north) / G.resolution);
    
    return new HPoint(deltaEast, deltaNorth);
};

/**
 * Get map row and column of center tile.
 *
 * @returns {Object} Object describing row and column of current center tile.
 * @private
 */
HMap.prototype.getCenterTile = function() {
    var G = this.globals;
    var tile = this.grid[0];

    // get center point in pixels
    var point = new HPoint(Math.round(G.mapSize.width / 2), Math.round(G.mapSize.height / 2));
    
    // delta distance in tiles to tile[0]
    var tileDeltaX = Math.floor((point.x - tile.point.x) / HTile.TILE_WIDTH);
    var tileDeltaY = -Math.floor((point.y - tile.point.y) / HTile.TILE_HEIGHT);
    
    // add delta distance
    var centerColumn = tile.mapColumn + tileDeltaX;
    var centerRow = tile.mapRow + tileDeltaY;    
   
    return {column: centerColumn, row: centerRow};
};

/**
 * Get map row and column of bottom left tile.
 *
 * @returns {Object} Object describing row and column of current bottom left tile.
 * @private
 */
HMap.prototype.getBottomLeftTile = function() {
    var index = 0;
    var point = this.grid[index].point;

    for (var i = 0; i < this.grid.length; i++) {
        if (this.grid[i].point.x <= point.x &&
            this.grid[i].point.y >= point.y) {
            point = this.grid[i].point;
            index = i;
        }
    }

    return {column: this.grid[index].mapColumn, row: this.grid[index].mapRow};
};

/**
 * Converts geographical coordinate from RT90 to latitude/longitude.
 *
 * @param {HPointRT90} rt90point RT90 point.
 * @returns {HPointLatLng} Coordinate as latitude/longitude.
 */
HMap.prototype.fromRT90ToLatLng = function(rt90point) {
    return this.transform.fromRT90ToLatLng(rt90point);
};

/**
 * Converts geographical coordinate from latitude/longitude to RT90.
 *
 * @param {HPointLatLng} pointLatLng Coordinate in latitude/longitude.
 * @returns {HPointRT90} Coordinate in RT90.
 */
HMap.prototype.fromLatLngToRT90 = function(pointLatLng) {
    return this.transform.fromLatLngToRT90(pointLatLng);
};

/**
 * Constructor for Map Provider Manager.
 * 
 * @class Manages the different MapProviders that serves tiles controlled by
 * the HTile object, making up map layers. MapProviders are added to an array
 * where map provider with index 0 is the bottom most layer.
 *
 * @param {HMap} hmap HMap instance.
 *
 * @constructor
 */
function HMapProviderManager(hmap) {
    this.hmap = hmap;
    this.providers = [];
}

/**
 * Add MapProvider to manager.
 *
 * @param {HMapProvider} provider Instance of a MapProvider.
 */
HMapProviderManager.prototype.add = function(provider) {
    this.insert(this.count(), provider);
};

/**
 * Insert MapProvider at given index into manager.
 *
 * @param {Number} index Index where to insert.
 * @param {HMapProvider} provider Instance of a MapProvider.
 */
HMapProviderManager.prototype.insert = function(index, provider) {
    this.providers.insert(index, provider);

    // create, update and position tiles of new layer
    if (this.hmap.globals.isInitialized) {
        this.providers[index].initTiles(this.hmap);

        for (var i = 0; i < this.hmap.grid.length; i++) {
            this.providers[index].updateTile(i, this.hmap.getResolution(), this.hmap.grid[i].mapRow, this.hmap.grid[i].mapColumn);
            this.providers[index].updatePosition(i, this.hmap.grid[i].point);
        }
    }
};

/**
 * Remove MapProvider.
 *
 * @param {HMapProvider} provider Instance of a MapProvider.
 */
HMapProviderManager.prototype.remove = function(provider) {
    provider.remove();
    this.providers.remove(provider);
};

/**
 * Returns number of added MapProviders.
 *
 * @returns {Number} Number of MapProviders.
 */
HMapProviderManager.prototype.count = function() {
    return this.providers.length;
};

/**
 * Returns index of given MapProvider.
 *
 * @param {HMapProvider} provider Instance of a MapProvider.
 * @returns {Number} Index of MapProvider.
 */
HMapProviderManager.prototype.indexOf = function(provider) {
    return this.providers.indexOf(provider);
};

/**
 * Get map provider with given index in manager.
 *
 * @param {Number} index Index of MapProvider in manager.
 * @returns {HMapProvider} Map Provider object.
 */
HMapProviderManager.prototype.get = function(index) {
    return this.providers[index];
};

/**
 * Get the base map provider that manages the bottom most map.
 * 
 * @returns {HMapProvider} Map Provider object.
 */
HMapProviderManager.prototype.getBaseMap = function() {
    return this.providers[0];
};

/**
 * Initiates tiles of given map provider.
 *
 * @private
 */
HMapProviderManager.prototype.initTiles = function() {
    for (var i = 0; i < this.providers.length; i++) {
        this.providers[i].initTiles(this.hmap);
    }
};

/**
 * Update pixel position for given tile in all map providers.
 *
 * @param {Number} tileIndex Index of tile to update.
 * @param {HPoint} point Point representing pixel position of tile.
 * @private
 */
HMapProviderManager.prototype.updatePosition = function(tileIndex, point) {
    for (var i = 0; i < this.providers.length; i++) {
        this.providers[i].updatePosition(tileIndex, point);
    }
};

/**
 * Update content of given tile in all map providers.
 *
 * @param {Number} tileIndex Index of tile to update.
 * @param {Number} resolution Current map resolution.
 * @param {Number} mapRow Current map row of tile.
 * @param {Number} mapColumn Current map column of tile.
 * @private
 */
HMapProviderManager.prototype.updateTile = function(tileIndex, resolution, mapRow, mapColumn) {
    for (var i = 0; i < this.providers.length; i++) {
        this.providers[i].updateTile(tileIndex, resolution, mapRow, mapColumn);
    }
};

/**
 * Trigger update of status in all map providers.
 * @private
 */
HMapProviderManager.prototype.updateStatus = function() {
    for (var i = 0; i < this.providers.length; i++) {
        this.providers[i].initStatusUpdate();
    }
};

/**
 * Constructor for HTile object.
 * 
 * @class The HTile object manages the position and content of a tile.
 *
 * @param {Object} hmap HMap object.
 * @param {Number} index Tile index.
 * @property {HPoint} point Current pixel position of tile.
 * @constructor
 * @private
 */
function HTile(hmap, index) {
    var G = hmap.globals;
    var me = this;
    
    me.point = new HPoint(-HTile.TILE_WIDTH, -HTile.TILE_HEIGHT);
    
    me.hmap = hmap;
    me.mapProviderManager = hmap.getMapProviderManager();
    me.tileIndex = index;
    me.tileColumn = index % G.tilesPerRow;  // column 0 is left most
    me.tileRow = (G.tilesPerColumn - 1) - parseInt( index / G.tilesPerRow ); // row 0 is bottom most
    
    me.initPosition();
}

/**
 * Tile width in pixels.
 *
 * @constant
 * @private
 */
HTile.TILE_WIDTH = 256;

/**
 * Tile height in pixels.
 *
 * @constant
 * @private
 */
HTile.TILE_HEIGHT = 256;

/**
 * Update tile position.
 */
HTile.prototype.initPosition = function() {
    var G = this.hmap.globals;

    // calculate map row/column of tile
    this.mapRow = G.mapRow + this.tileRow;
    this.mapColumn = G.mapColumn + this.tileColumn;

    // calculate absolute pixel position of tile
    this.point.x = G.tileInitOffsetX + this.tileColumn * HTile.TILE_WIDTH;
    this.point.y = G.tileInitOffsetY + ((G.tilesPerColumn - 1) - this.tileRow) * HTile.TILE_HEIGHT;

    this.mapProviderManager.updatePosition(this.tileIndex, this.point);
};

/**
 * Update map tile.
 */
HTile.prototype.updateMapTile = function() {
    var G = this.hmap.globals;

    this.mapProviderManager.updateTile(this.tileIndex, G.resolution, this.mapRow, this.mapColumn);
};

/**
 * Pan tile.
 *
 * @param {Number} dx Delta X movement in pixels to pan.
 * @param {Number} dy Delta Y movement in pixels to pan.
 * @returns {Boolean} True if pan caused tile to flip side.
 */
HTile.prototype.pan = function(dx, dy) {
    var me = this;
    var G = me.hmap.globals;
    
    me.point.x += dx;
    me.point.y += dy;
    
    var flipped = false;
    
    if (me.point.x > G.mapSize.width + G.mapPaddingX - (HTile.TILE_WIDTH / 2)) {
        // moved out to the right, flip to left side
        me.point.x -= G.tilesPerRow * HTile.TILE_WIDTH;
        me.mapColumn -= G.tilesPerRow;
        flipped = true;
    } else if (me.point.x <= -HTile.TILE_WIDTH - G.mapPaddingX + (HTile.TILE_WIDTH / 2)) {
        // moved out to the left, flip to right side
        me.point.x += G.tilesPerRow * HTile.TILE_WIDTH;
        me.mapColumn += G.tilesPerRow;
        flipped = true;
    }

    if (me.point.y > G.mapSize.height + G.mapPaddingY - (HTile.TILE_HEIGHT / 2)) {
        // moved out to the bottom, flip to top
        me.point.y -= G.tilesPerColumn * HTile.TILE_HEIGHT;
        me.mapRow += G.tilesPerColumn;
        flipped = true;
    } else if (me.point.y <= -HTile.TILE_HEIGHT - G.mapPaddingY + (HTile.TILE_HEIGHT / 2)) {
        // moved out to the top, flip to bottom
        me.point.y += G.tilesPerColumn * HTile.TILE_HEIGHT;
        me.mapRow -= G.tilesPerColumn;
        flipped = true;
    }

    this.mapProviderManager.updatePosition(this.tileIndex, me.point);

    if (flipped) {
        me.updateMapTile();
    }

    return flipped;
};
/**
 * @fileOverview Default control interface and implementations.
 */

/**
 * HControl constructor.
 * 
 * @class HControl must be implemented by all controls.
 *
 * @constructor
 */
function HControl() {
    
}

/**
 * Z-index used for controls.
 * 
 * @constant
 * @private
 */
HControl.Z_INDEX = 10000000;

/**
 * Initiate control.
 *
 * @param {HMap} hmap The HMap instance.
 * @param {HControlPosition} position Position of control.
 */
HControl.prototype.init = function(hmap, position) {
    this.hmap = hmap;
    this.position = position;

    // append control to container
    this.container = this.hmap.getPaneControls();
    this.container.appendChild(this.element);

    this.update();
}

/**
 * Called when control is removed from map.
 */
HControl.prototype.remove = function() {
    this.container.removeChild(this.element);
};

/**
 * Get the DOM element used for the control.
 *
 * @returns {Element} Element used for control.
 */
HControl.prototype.getElement = function() {
    return this.element;
};

/**
 * Updates position of control. May only be called after control is added
 * to map.
 */
HControl.prototype.update = function() {
    var pos = this.getContainerPosition();

    this.element.style.left = pos.x + "px";
    this.element.style.top = pos.y + "px";
};

/**
 * Calculates container position of control.
 *
 * @returns {HPoint}
 * @private
 */
HControl.prototype.getContainerPosition = function() {
    var size = this.hmap.getSize();

    var G = this.hmap.globals;

    var pos = new HPoint(0, 0);

    switch (this.position.anchor) {
        case HControlPosition.ANCHOR_TOP_RIGHT:
            pos.x = size.width - G.paddingRight;
            pos.y = 0 + G.paddingTop;
            break;
        case HControlPosition.ANCHOR_TOP_CENTER:
            pos.x = G.paddingLeft + Math.round((size.width - G.paddingRight - G.paddingLeft) / 2);
            pos.y = 0 + G.paddingTop;
            break;
        case HControlPosition.ANCHOR_TOP_LEFT:
            pos.x = 0 + G.paddingLeft;
            pos.y = 0 + G.paddingTop;
            break;
        case HControlPosition.ANCHOR_LEFT_CENTER:
            pos.x = 0 + G.paddingLeft;
            pos.y = G.paddingTop + Math.round((size.height - G.paddingTop - G.paddingBottom) / 2);
            break;
        case HControlPosition.ANCHOR_RIGHT_CENTER:
            pos.x = size.width - G.paddingRight;
            pos.y = G.paddingTop + Math.round((size.height - G.paddingTop - G.paddingBottom) / 2);
            break;
        case HControlPosition.ANCHOR_BOTTOM_RIGHT:
            pos.x = size.width - G.paddingRight;
            pos.y = size.height - G.paddingBottom;
            break;
        case HControlPosition.ANCHOR_BOTTOM_CENTER:
            pos.x = G.paddingLeft + Math.round((size.width - G.paddingRight - G.paddingLeft) / 2 );
            pos.y = size.height - G.paddingBottom;
            break;
        case HControlPosition.ANCHOR_BOTTOM_LEFT:
            pos.x = 0 + G.paddingLeft;
            pos.y = size.height - G.paddingBottom;
            break;
        case HControlPosition.ANCHOR_CENTER:
            pos.x = Math.round( ((size.width - G.paddingLeft - G.paddingRight) - this.element.offsetWidth)/2 + G.paddingLeft );
            pos.y = Math.round( ((size.height - G.paddingTop - G.paddingBottom) - this.element.offsetHeight)/2 + G.paddingTop );
            break;
    }

    pos.x += this.position.offset.x;
    pos.y += this.position.offset.y;

    return pos;
};

/**
 * Control default position.
 *
 * @returns {HControlPosition}
 */
HControl.prototype.getDefaultPosition = function() {
    return new HControlPosition(HControlPosition.ANCHOR_TOP_LEFT);
};

/**
 * A control is anchored to an edge or corner of the map view with an offset to
 * determine position.
 *
 * @class The control position object is used by HControl descendants to
 * determine where they should be positioned in the map container.
 *
 * @param {Number} anchor Where control is anchored.
 * @param {HPoint} [offsetPoint] Offset in pixels.
 *
 * @constructor
 */
function HControlPosition(anchor, offsetPoint) {
    this.anchor = anchor;
    this.offset = arguments[1] || new HPoint(0,0);
}

HControlPosition.ANCHOR_TOP_RIGHT = 1;
HControlPosition.ANCHOR_TOP_CENTER = 2;
HControlPosition.ANCHOR_TOP_LEFT = 3;
HControlPosition.ANCHOR_LEFT_CENTER = 4;
HControlPosition.ANCHOR_RIGHT_CENTER = 5;
HControlPosition.ANCHOR_BOTTOM_RIGHT = 6;
HControlPosition.ANCHOR_BOTTOM_CENTER = 7;
HControlPosition.ANCHOR_BOTTOM_LEFT = 8;
HControlPosition.ANCHOR_CENTER = 9;

/**
 * Zoom control constructor.
 *
 * @class The zoom control with buttons to zoom in and out and a draggable handler.
 *
 * @augments HControl
 * @constructor
 */
function HZoomControl() {
    var div = document.createElement("div");
    div.id = "HZoomControl";

    HMapHelpers.applyStyle([div], {
        position: "absolute",
        fontFamily: "Verdana, Arial",
        fontSize: "12px",
        fontWeight: "normal",
        zIndex: HControl.Z_INDEX
    });

    // zoom out button
    var zoomOutButton = document.createElement("img");
    zoomOutButton.id = "HzoomOutButton";
    zoomOutButton.src = HZoomControl.GFX_URL + "zoom_out.png";
    HMapHelpers.applyStyle([zoomOutButton], {
        position: "absolute",
        cursor: "pointer",
        width: "84px",
        height: "20px"
    });

    // zoom in button
    var zoomInButton = document.createElement("img");
    zoomInButton.id = "HzoomInButton";
    zoomInButton.src = HZoomControl.GFX_URL + "zoom_in.png";
    HMapHelpers.applyStyle([zoomInButton], {
        position: "absolute",
        cursor: "pointer",
        width: "84px",
        height: "20px",
        left: "285px"
    });

    // container for zoom level icons
    var zoomLevels = document.createElement("div");
    HMapHelpers.applyStyle([zoomLevels], {
        position: "absolute",
        left: "87px"
    });

    // background frame
    var bkgImg = document.createElement("img");
    bkgImg.id = "HzoomBackground"
    bkgImg.src = HZoomControl.GFX_URL + "zoom_bkg.png";
    HMapHelpers.applyStyle([bkgImg], {
        position: "absolute",
        cursor: "auto"
    });
    zoomLevels.appendChild(bkgImg);

    // zoom icons for each zoom level
    this.zoomIcons = [];
    for (var i = 1; i <= HZoomControl.ZOOM_LEVELS; i++) {
        this.zoomIcons[i] = document.createElement("div");

        this.zoomIcons[i].id = "HzoomIcon" + i;
        this.zoomIcons[i].innerHTML = i;

        HMapHelpers.applyStyle([this.zoomIcons[i]], {
            position: "absolute",
            backgroundColor: "#fff",
            fontSize: "11px",
            border: "1px solid #848CA1",
            color: "#898B86",
            cursor: "pointer",
            zIndex: HControl.Z_INDEX,
            textAlign: "center",
            width: "14px",
            height: "14px",
            left: (HZoomControl.ZOOM_LEVEL_OFFSET + HZoomControl.ZOOM_LEVEL_WIDTH*(i-1)) + "px",
            top: "2px"
        });

        zoomLevels.appendChild(this.zoomIcons[i]);
    }

    // draggable zoom handle container
    var zoomHandle = document.createElement("div");
    HMapHelpers.applyStyle([zoomHandle], {
        position: "absolute",
        left: "21px",
        top: "-3px",
        zIndex: (HControl.Z_INDEX + 1)
    });

    // image of zoom handle
    var zoomHandleImg = document.createElement("img");
    zoomHandleImg.src = HZoomControl.GFX_URL + "zoom_handle.png";
    zoomHandle.appendChild(zoomHandleImg);

    // number on top of zoom handle indicating current zoom level
    var zoomHandleNumber = document.createElement("div");
    HMapHelpers.applyStyle([zoomHandleNumber], {
        position: "absolute",
        left: "-1px",
        top: "6px",
        width: HZoomControl.ZOOM_HANDLE_WIDTH + "px",
        height: "28px",
        textAlign: "center",
        fontSize: "11px",
        cursor: "pointer",
        zIndex: (HControl.Z_INDEX + 1)
    });
    zoomHandle.appendChild(zoomHandleNumber);

    zoomLevels.appendChild(zoomHandle);
    
    div.appendChild(zoomOutButton);
    div.appendChild(zoomLevels);
    div.appendChild(zoomInButton);

    this.zoomHandle = zoomHandle;
    this.zoomHandleNumber = zoomHandleNumber;
    this.element = div;
}

// HZoomControl inherits from HControl
HZoomControl.prototype = new HControl();

HZoomControl.GFX_URL = "http://www.hitta.se/Images/hmap/";          // URL prefix
HZoomControl.ZOOM_LEVELS = 10;          // available zoom levels
HZoomControl.WIDTH = 368;               // width of zoom control (to calculate position)
HZoomControl.ZOOM_LEVEL_OFFSET = 3;     // horizontal offset for zoom level numbers
HZoomControl.ZOOM_LEVEL_WIDTH = 19;     // spacing between zoom level numbers
HZoomControl.ZOOM_HANDLE_WIDTH = 18;    // width of zoom handle image
HZoomControl.ZOOM_LEVEL_CONTAINER_WIDTH = HZoomControl.ZOOM_LEVEL_OFFSET + HZoomControl.ZOOM_LEVELS * HZoomControl.ZOOM_LEVEL_WIDTH;
HZoomControl.ANIM_INTERVAL = 10;        // timer interval for zoom handle animation
HZoomControl.ANIM_SPEED = 5;            // lower = faster

/**
 * Initiate zoom control.
 *
 * @param {HMap} hmap The HMap instance.
 * @param {HControlPosition} position Position of control.
 * @private
 */
HZoomControl.prototype.init = function(hmap, position) {
    var me = this;

    // call overridden init in "super class"
    HControl.prototype.init.apply(me, [hmap, position]);

    var zoomOutButton = document.getElementById("HzoomOutButton");
    var zoomInButton = document.getElementById("HzoomInButton");

    // listen to clicks on zoom buttons
    HEvent.addDomListener(zoomOutButton, "click", function(e) {
        me.hmap.zoomOut();
        HEvent.stopEvent(e);
    });
    HEvent.addDomListener(zoomInButton, "click", function(e) {
        me.hmap.zoomIn();
        HEvent.stopEvent(e);
    });
    
    // stop click on zoom background
    var zoomBkg = document.getElementById("HzoomBackground");
    HEvent.addDomListener(zoomBkg, "click", HEvent.stopEvent);
    HEvent.addDomListener(zoomBkg, "dblclick", HEvent.stopEvent);
    
    // stop double click events on zoom buttons
    HEvent.addDomListener(zoomOutButton, "dblclick", HEvent.stopEvent);
    HEvent.addDomListener(zoomInButton, "dblclick", HEvent.stopEvent);

    // click on zoom level buttons
    for (var i = 1; i <= HZoomControl.ZOOM_LEVELS; i++) (function(i) {
        HEvent.addDomListener(me.zoomIcons[i], "click", function(e) {
            me.hmap.setZoom(i);
            me.isDragging = false;
            HEvent.stopEvent(e);
        });
    })(i);

    // zoom handle event listeners
    HEvent.addDomListener(this.zoomHandle, "mousedown", function(e) { me.startZoomHandleDrag(e); });
    HEvent.addDomListener(document, "mouseup", function(e) { me.stopZoomHandleDrag(e); });
    HEvent.addDomListener(document, "mousemove", function(e) { me.zoomHandleDrag(e); });

    this.isDragging = false;

    // init with current zoom level
    this.updateZoomLevel(hmap.getZoom(), true);

    // listen to zoom changes
    HEvent.addListener(me.hmap, HMap.EVENT_INTERNAL_MAP_ZOOM, function(oldIndex, newIndex) { me.updateZoomLevel(newIndex); } );
    HEvent.addListener(me.hmap, HMap.EVENT_INTERNAL_MAP_TYPE_CHANGED, function() { me.updateStatus(null); } );
    HEvent.addListener(HStandardMapProvider, HMap.EVENT_INTERNAL_MAP_STATUS_UPDATE, function(statusObj) { me.updateStatus(statusObj); } );
};

/**
 * Listener to zoom level change event. Updates status of zoom control using
 * animation.
 *
 * @param {Number} newLevel Current zoom level.
 * @param {Boolean} [skipAnim] Set to true to skip animation.
 * @private
 */
HZoomControl.prototype.updateZoomLevel = function(newLevel, skipAnim) {
    var me = this;

    var current = me.zoomHandle.offsetLeft;
    var dest = HZoomControl.ZOOM_LEVEL_OFFSET + (HZoomControl.ZOOM_LEVEL_WIDTH * (newLevel - 1));

    if (arguments[1] || false) {
        // skip animation
        me.zoomHandle.style.left = dest + "px";
        me.zoomHandleNumber.innerHTML = newLevel;
    } else {
        // animate movement
        var timer = setInterval(function() {
            var step = (dest - current) / HZoomControl.ANIM_SPEED;
            step = (step < 0) ? Math.floor(step) : Math.ceil(step);

            current += step;
            me.zoomHandle.style.left = current + "px";

            if (current == dest) {
                clearInterval(timer);
                me.zoomHandleNumber.innerHTML = newLevel;
            }
        }, HZoomControl.ANIM_INTERVAL);
    }
};

/**
 * Update zoom level status of zoom control. Call with JSON status object or
 * when map type is changed.
 *
 * @param {Object} statusObj JSON zoom level status object. If null update
 *                           will use previous status object but check current
 *                           map type.
 * @private
 */
HZoomControl.prototype.updateStatus = function(statusObj) {
    if (statusObj != null) {
        // save status
        this.currentStatus = statusObj;
    }

    // update available zoom levels
    if (this.currentStatus) {
        var mapType = this.hmap.getMapType();

        for (var i = 1; i <= HZoomControl.ZOOM_LEVELS; i++)  {
            if (i >= this.currentStatus[mapType].min &&
                i <=  this.currentStatus[mapType].max) {
                // zoom level available
                HMapHelpers.applyStyle([this.zoomIcons[i]], {
                    backgroundColor: "#fff"
                });
            } else {
                // zoom level not available
                HMapHelpers.applyStyle([this.zoomIcons[i]], {
                    backgroundColor: "#C0C0C0"
                });
            }
        }
    }
};

/**
 * Event listener to start drag of zoom handle.
 *
 * @param {Event} e Mouse event.
 * @private
 */
HZoomControl.prototype.startZoomHandleDrag = function(e) {    
    var pos = HMapHelpers.getMousePosition(e);
    HEvent.stopEvent(e);

    this.dragStartX = pos.x;
    this.dragHandleStartX = this.zoomHandle.offsetLeft;
    this.zoomHandleNumber.innerHTML = "";

    this.isDragging = true;
};

/**
 * Event listener to stop drag of zoom handle.
 *
 * @param {Event} e Mouse event.
 * @private
 */
HZoomControl.prototype.stopZoomHandleDrag = function(e) {
    if (this.isDragging) {
        var pos = HMapHelpers.getMousePosition(e);
        var left = this.restrictZoomHandleBounds( this.dragHandleStartX + (pos.x - this.dragStartX) );

        // calculate zoom level where handle is dropped on
        var level = 1 + Math.round((left - HZoomControl.ZOOM_LEVEL_OFFSET) / HZoomControl.ZOOM_LEVEL_WIDTH);

        if (this.hmap.getZoom() != level) {
            // if invalid zoom level, zoom to max
            if (!this.hmap.setZoom(level)) {
                this.hmap.zoomMax();
            }
        }

        this.isDragging = false;
    }
};

/**
 * Event listener to manage mouse movement when dragging the zoom handle.
 *
 * @param {Event} e Mouse event.
 * @private
 */
HZoomControl.prototype.zoomHandleDrag = function(e) {
    if (this.isDragging) {
        var pos = HMapHelpers.getMousePosition(e);
        var left = this.restrictZoomHandleBounds( this.dragHandleStartX + (pos.x - this.dragStartX) );

        this.zoomHandle.style.left = left + "px";

        HEvent.stopEvent(e);
    }
};

/**
 * Restrict horizontal pixel coordinates of zoom handle position to make sure it is
 * within the bounds of the zoom control.
 *
 * @param {Number} left Left coordinate in pixels.
 * @returns {Number} Left coordinate capped.
 * @private
 */
HZoomControl.prototype.restrictZoomHandleBounds = function(left) {
    // check left bound
    if (left < HZoomControl.ZOOM_LEVEL_OFFSET) {
        left = HZoomControl.ZOOM_LEVEL_OFFSET;
    }

    // check right bound
    if (left >= HZoomControl.ZOOM_LEVEL_CONTAINER_WIDTH - HZoomControl.ZOOM_HANDLE_WIDTH) {
        left = HZoomControl.ZOOM_LEVEL_CONTAINER_WIDTH - HZoomControl.ZOOM_HANDLE_WIDTH;
    }
    
    return left;
}

/**
 * Get default position of zoom control.
 *
 * @returns {HControlPosition} Control position object.
 */
HZoomControl.prototype.getDefaultPosition = function() {
    return new HControlPosition(HControlPosition.ANCHOR_BOTTOM_CENTER, new HPoint(-Math.round(HZoomControl.WIDTH/2), -25));
};

/**
 * Scale control constuctor.
 *
 * @class The scale control displays the current map scale (pixel / distance ratio).
 *
 * @augments HControl
 * @constructor
 */
function HScaleControl() {
    var div = document.createElement("div");
    div.id = "HScaleControl";

    HMapHelpers.applyStyle([div], {
        position: "absolute",
        borderStyle: "none solid solid",
        borderWidth: "medium 2px 2px",
        margin: "2px 0 5px 0",
        fontFamily: "Arial",
        fontSize: "12px",
        textAlign: "center",
        zIndex: HControl.Z_INDEX,
        width: "100px"
    });

    this.element = div;
}

// HScaleControl inherits from HControl
HScaleControl.prototype = new HControl();

/**
 * Default width of scale control
 *
 * @constant
 * @private
 */
HScaleControl.DEFAULT_WIDTH = 100;

/**
 * Scale control configuration per zoom level
 *
 * @constant
 * @private
 */
HScaleControl.SCALE_CONFIG = {
    1: {dist: "35mil", width: HScaleControl.DEFAULT_WIDTH}, // 3500
    2: {dist: "70km", width: HScaleControl.DEFAULT_WIDTH},  //  700
    3: {dist: "20km", width: HScaleControl.DEFAULT_WIDTH},  //  200
    4: {dist: "7km", width: HScaleControl.DEFAULT_WIDTH},   //   70
    5: {dist: "2km", width: 80},                            //   25
    6: {dist: "1km", width: HScaleControl.DEFAULT_WIDTH},   //   10
    7: {dist: "400m", width: HScaleControl.DEFAULT_WIDTH},  //    4
    8: {dist: "200m", width: HScaleControl.DEFAULT_WIDTH},  //    2
    9: {dist: "50m", width: HScaleControl.DEFAULT_WIDTH},   //  0.5
   10: {dist: "20m", width: HScaleControl.DEFAULT_WIDTH}    //  0.2
};

/**
 * Initiate scale control.
 *
 * @param {HMap} hmap The HMap instance.
 * @param {HControlPosition} position Position of control.
 */
HScaleControl.prototype.init = function(hmap, position) {
    var me = this;

    // call overridden init in "super class"
    HControl.prototype.init.apply(me, [hmap, position]);

    this.updateZoomLevel(0, hmap.getZoom());

    HEvent.addListener(me.hmap, HMap.EVENT_INTERNAL_MAP_ZOOM, function(oldIndex, newIndex) { me.updateZoomLevel(oldIndex, newIndex); } );
};

/**
 * Listener to zoom level change event. Updates size and text of scale control.
 *
 * @param {Number} oldLevel Old zoom level.
 * @param {Number} newLevel Current zoom level.
 */
HScaleControl.prototype.updateZoomLevel = function(oldLevel, newLevel) {
    this.element.style.width = HScaleControl.SCALE_CONFIG[newLevel].width;
    this.element.innerHTML = HScaleControl.SCALE_CONFIG[newLevel].dist;

    this.element.style.marginLeft = HScaleControl.DEFAULT_WIDTH - HScaleControl.SCALE_CONFIG[newLevel].width;

    // update position (since width may change and it by default right aligned)
    this.update();
};

/**
 * Get default position of scale control.
 *
 * @returns {HControlPosition} Control position object.
 */
HScaleControl.prototype.getDefaultPosition = function() {
    return new HControlPosition(HControlPosition.ANCHOR_BOTTOM_RIGHT, new HPoint(-106, -24));
};

/**
 * Copyright control constructor.
 *
 * @class The copyright control displays copyright info.
 *
 * @augments HControl
 * @constructor
 */
function HCopyrightControl() {
    var div = document.createElement("div");

    HMapHelpers.applyStyle([div], {
        position: "absolute",
        fontFamily: "Arial",
        fontSize: "10px",
        zIndex: HControl.Z_INDEX
    });

    div.innerHTML = "&copy Lantm&auml;teriverket";

    this.element = div;
}

// HCopyrightControl inherits from HControl
HCopyrightControl.prototype = new HControl();

/**
 * Get default position of copyright control.
 *
 * @returns {HControlPosition} Control position object.
 */
HCopyrightControl.prototype.getDefaultPosition = function() {
    return new HControlPosition(HControlPosition.ANCHOR_BOTTOM_LEFT, new HPoint(2, -20));
};

/**
 * Logo control constructor.
 *
 * @class The logo control displays a given image on top of the map view.
 *
 * @param {String} imgUrl URL to image.
 * @param {Number} width Width of image.
 * @param {Number} height Height of image.
 * @augments HControl
 * @constructor
 *
 * @example
 * var logo = new HLogoControl("http://www.hitta.se/images/LogoTest.gif", 67, 67);
 * map.addControl(logo);
 */
function HLogoControl(imgUrl, width, height) {
    this.width = width;
    this.height = height;
    this.onClick = null;
    
    var img = document.createElement("img");

    img.id = "HLogoControl";
    img.src = imgUrl;
    img.className = "link";

    if (HMapHelpers.isIEVersionBelow(7)) {
        HMapHelpers.applyIEPNGFix(img, imgUrl);
    }

    HMapHelpers.applyStyle([img], {
        position: "absolute",
        width: width + "px",
        height: height + "px",
        zIndex: HControl.Z_INDEX
    });

    this.onClick = HEvent.addDomListener(img, 'click', function(e) {
        location.href = 'http://www.hitta.se';
        HEvent.stopEvent(e);
    })
    this.element = img;
}

// HLogoControl inherits from HControl
HLogoControl.prototype = new HControl();

/**
 * Default vertical padding from bottom (this should include the height
 * of the copyright control.
 * @constant
 * @private
 */
HLogoControl.DEFAULT_VERT_PADDING = -25;

/**
 * Default vertical padding from bottom (this should include the height
 * of the copyright control.
 * @constant
 * @private
 */
HLogoControl.DEFAULT_HORZ_PADDING = 4;

/**
 * Default position of logo control is lower left corner. This may be
 * overridden by defining a new position when invoking HMap.addControl().
 *
 * @returns {HControlPosition} Control position object.
 */
HLogoControl.prototype.getDefaultPosition = function() {
    return new HControlPosition(
        HControlPosition.ANCHOR_BOTTOM_LEFT,
        new HPoint(
            HLogoControl.DEFAULT_HORZ_PADDING,
            HLogoControl.DEFAULT_VERT_PADDING - this.height
        )
    );
};

/**
 * Map type control constructor.
 *
 * @class The map type control displays a buttons to switch between normal
 *        and satellite map type.
 *
 * @augments HControl
 * @constructor
 */
function HMapTypeControl() {
    var controlElement = document.createElement("span");
    controlElement.id = "HMapTypeControl";

    HMapHelpers.applyStyle([controlElement], {
        border: "1px solid #222",
        position: "absolute",
        fontFamily: "Arial",
        fontSize: "11px",
        zIndex: HControl.Z_INDEX,
        cursor: "pointer"
    });

    var mapNormal = document.createElement("span");
    HMapHelpers.applyStyle([mapNormal], {
        zIndex: HControl.Z_INDEX,
        padding: "2px 5px 2px 5px",
        "float": "left"
    });

    mapNormal.innerHTML = "Karta";

    var mapSatellite = document.createElement("span");
    HMapHelpers.applyStyle([mapSatellite], {
        zIndex: HControl.Z_INDEX,
        padding: "2px 5px 2px 5px",
        "float": "left"
    });

    mapSatellite.innerHTML = "Satellit";

    this.options = [];
    this.options[HStandardMapProvider.MAP_TYPE_NORMAL] = mapNormal;
    this.options[HStandardMapProvider.MAP_TYPE_SATELLITE] = mapSatellite;

    this.makeSelected(HStandardMapProvider.MAP_TYPE_NORMAL);

    var me = this;

    HEvent.addDomListener(mapNormal, "click", function(e) { me.changeTypeTo(HStandardMapProvider.MAP_TYPE_NORMAL); return HEvent.stopEvent(e); } );
    HEvent.addDomListener(mapSatellite, "click", function(e) {me.changeTypeTo(HStandardMapProvider.MAP_TYPE_SATELLITE); return HEvent.stopEvent(e); } );

    controlElement.appendChild(mapNormal);
    controlElement.appendChild(mapSatellite);

    this.element = controlElement;
}

// HMapTypeControl inherits from HControl
HMapTypeControl.prototype = new HControl();

/**
 * Initiate map type control.
 *
 * @param {HMap} hmap The HMap instance.
 * @param {HControlPosition} position Position of control.
 * @private
 */
HMapTypeControl.prototype.init = function(hmap, position) {
    var me = this;

    // call overridden init in "super class"
    HControl.prototype.init.apply(me, [hmap, position]);

    // automatically select current map type
    this.changeTypeTo(hmap.getMapType());
};

/**
 * Change map type.
 *
 * @param {Number} mapType Map type to change to.
 * @private
 */
HMapTypeControl.prototype.changeTypeTo = function(mapType) {
    this.makeSelected(mapType);

    this.hmap.setMapType(mapType);
};

/**
 * Update visual style of button to make given map type appear selected and
 * the others unselected.
 * 
 * @param {Number} mapType Map type to change to.
 * @private
 */
HMapTypeControl.prototype.makeSelected = function(mapType) {
    for (var i = 0; i < this.options.length; i++) {
        if (mapType == i) {
            // selected
            HMapHelpers.applyStyle([this.options[i]], {
                background: "#fff",
                border: "1px solid #ccc"
            });
        } else {
            // unselected
            HMapHelpers.applyStyle([this.options[i]], {
                background: "#ccc",
                border: "1px solid #fff"
            });
        }
    }
};

/**
 * Default position of map type control is upper right corner. This may be
 * overridden by defining a new position when invoking HMap.addControl().
 *
 * @returns {HControlPosition} Control position object.
 */
HMapTypeControl.prototype.getDefaultPosition = function() {
    return new HControlPosition(
        HControlPosition.ANCHOR_TOP_LEFT,
        new HPoint(0, 0)
    );
};

/**
 * Small zoom control constructor.
 *
 * @class Small zoom control with buttons to zoom in and out.
 *
 * @augments HControl
 * @constructor
 */
function HSmallZoomControl() {
    var div = document.createElement("div");
    div.id = "HSmallZoomControl";

    HMapHelpers.applyStyle([div], {
        position: "absolute",
        zIndex: HControl.Z_INDEX
    });

    var zoomOut = document.createElement("img");
    zoomOut.src = HSmallZoomControl.GFX_URL + "zoom_out_small.png";

    HMapHelpers.applyStyle([zoomOut], {
        cursor: "pointer",
        zIndex: HControl.Z_INDEX
    });

    var zoomIn = document.createElement("img");
    zoomIn.src = HSmallZoomControl.GFX_URL + "zoom_in_small.png";

    HMapHelpers.applyStyle([zoomIn], {
        cursor: "pointer",
        zIndex: HControl.Z_INDEX
    });


    var me = this;

    // listen to clicks on zoom buttons
    HEvent.addDomListener(zoomOut, "click", function(e) {
        me.zoomOut();
        HEvent.stopEvent(e);
    });
    HEvent.addDomListener(zoomIn, "click", function(e) {
        me.zoomIn();
        HEvent.stopEvent(e);
    });

    HEvent.addDomListener(zoomOut, "dblclick", HEvent.stopEvent);
    HEvent.addDomListener(zoomIn, "dblclick", HEvent.stopEvent);

    div.appendChild(zoomOut);
    div.appendChild(zoomIn);

    this.element = div;
}

// HSmallZoomControl inherits from HControl
HSmallZoomControl.prototype = new HControl();

HSmallZoomControl.WIDTH = 30;
HSmallZoomControl.GFX_URL = "http://www.hitta.se/Images/hmap/";          // URL prefix

/**
 * Zoom in.
 * @private
 */
HSmallZoomControl.prototype.zoomIn = function() {
    this.hmap.zoomIn();
};

/**
 * Zoom out.
 * @private
 */
HSmallZoomControl.prototype.zoomOut = function() {
    this.hmap.zoomOut();
};

/**
 * Get default position of small zoom control.
 *
 * @returns {HControlPosition} Control position object.
 */
HSmallZoomControl.prototype.getDefaultPosition = function() {
    return new HControlPosition(HControlPosition.ANCHOR_BOTTOM_CENTER, new HPoint(-Math.round(HSmallZoomControl.WIDTH/2), -20));
};


/**
 * Label control constructor.
 *
 * @class The label control displays text (or whatever HTML set as parameter in
 * the constructor) at given position (default center).
 *
 * @param {String} html HTML string as label content.
 * @param {HControlPosition} [position] Optional position object.
 *
 * @augments HControl
 * @constructor
 */
function HLabel(html, position) {
    this.position = arguments[1] || new HControlPosition(HControlPosition.ANCHOR_CENTER);

    var div = document.createElement("div");
    div.innerHTML = html;

    HMapHelpers.applyStyle([div], {
        position: "absolute"
    });

    this.element = div;
}

// HLabel inherits from HControl
HLabel.prototype = new HControl();

/**
 * Get default position of label control.
 *
 * @returns {HControlPosition} Control position object.
 */
HLabel.prototype.getDefaultPosition = function() {
    return this.position;
};

/**
 * Pan control constructor.
 *
 * @class The pan control displays arrow that when clicked pan the map.
 *
 * @augments HControl
 * @constructor
 */
function HPanControl() {
    var img = document.createElement("img");
    img.id = "HPanControl";
    img.src = "http://www.hitta.se/Images/hmap/nav-blue.png";

    if (HMapHelpers.isIEVersionBelow(7)) {
        HMapHelpers.applyIEPNGFix(img, img.src);
    }

    HMapHelpers.applyStyle([img], {
        position: "absolute",
        width: "50px",
        height: "50px",
        cursor: "pointer",
        zIndex: HControl.Z_INDEX
    });

    var areas = {
        up: {x1: 13, y1: 0, x2: 36, y2: 13},
        right: {x1: 35, y1: 12, x2: 49, y2: 35},
        down: {x1: 13, y1: 34, x2: 36, y2: 48},
        left: {x1: 0, y1: 12, x2: 14, y2: 35}
    };

    var me = this;
    HEvent.addDomListener(img, "click", function(e) {
        var contains = function(pos, box) {
            return (pos.x > box.x1 && pos.x < box.x2 && pos.y > box.y1 && pos.y < box.y2);
        }

        var pos = HMapHelpers.getMousePosition(e);
        var ePos = HMapHelpers.getElementPosition(img);

        pos.x -= ePos.x;
        pos.y -= ePos.y;

        if (contains(pos, areas.up)) {
            me.hmap.panDirection(0, +1);
        } else if (contains(pos, areas.right)) {
            me.hmap.panDirection(-1, 0);
        } else if (contains(pos, areas.down)) {
            me.hmap.panDirection(0, -1);
        } else if (contains(pos, areas.left)) {
            me.hmap.panDirection(+1, 0);
        }

        return HEvent.stopEvent(e);
    });

    this.element = img;
}

// HPanControl inherits from HControl
HPanControl.prototype = new HControl();

/**
 * Get default position of pan control.
 *
 * @returns {HControlPosition} Control position object.
 */
HPanControl.prototype.getDefaultPosition = function() {
    return new HControlPosition(HControlPosition.ANCHOR_BOTTOM_LEFT, new HPoint(5, -75));
};
/**
 * @fileOverview Static helper functions used by HMap implementation.
 */

/**
 * Static helper functions used by HMap.
 * @namespace
 */
var HMapHelpers = {};

/**
 * Creates a DIV element to be used as element container in the map and adds
 * it as a child to given parent element.
 *
 * @param {Element} parent Parent element.
 * @returns {Object} The created DIV element.
 */
HMapHelpers.createContainerDiv = function(parent) {
    var container = document.createElement("div");
    
    HMapHelpers.applyStyle([container], {
        position: "absolute",
        overflow: "hidden",
        textAlign: "left",
        width: "100%",
        height: "100%",
        left: "0px",
        top: "0px",
        UserSelect: "none",
        KhtmlUserSelect: "none",
        MozUserSelect: "none",
        cursor: "move"
    });
    
    parent.appendChild(container);
    
    return container;
};

/**
 * Creates a DIV element containing the given icon as background
 * This function will honor {@link HIcon#iconIndex} and hence support sprite-icons.
 *
 * @param {HIcon} hIcon The icon to create an element for.
 * @returns {Object} The created DIV element.
 */
HMapHelpers.createIconElement = function(hIcon){

    var element = document.createElement("div");

    HMapHelpers.applyStyle([element], {
                            height: hIcon.size.height + "px",
                            width: hIcon.size.width + "px",
                            overflow: "hidden"});

    var spriteOffset = hIcon.size.width * hIcon.iconIndex;

    if (HMapHelpers.isIEVersionBelow(7)) {
        var imgElement = document.createElement("div");
        HMapHelpers.applyStyle([imgElement],
                            {position: "relative",
                             display: "block",
                             left: -spriteOffset + "px",
                             height: hIcon.size.height + "px",
                             width: hIcon.size.width + "px"});
        
        HMapHelpers.applyIEPNGFix(imgElement, hIcon.image);
        element.appendChild(imgElement);

    } else {
        HMapHelpers.applyBackgroundImage(element, hIcon.image);
        HMapHelpers.setBackgroundPosition(element, -spriteOffset + "px", "0px")
    }
    return element;
}
/**
 * Clear an element by removing all its child nodes.
 *
 * @param {Element} element Element to remove all child nodes from.
 */
HMapHelpers.clearElement = function(element) {
    while (element.hasChildNodes()) {
        element.removeChild(element.lastChild);
    }
};

/**
 * Get mouse position.
 *
 * @param {Object} event Event object.
 * @return {Object} Position object.
 */
HMapHelpers.getMousePosition = function(event) {
    var e = event || window.event;
    
    var mouseX = e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft));
    var mouseY = e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop));
    
    return new HPoint(mouseX, mouseY);
};

/**
 * Get position of element through parent hierarchy.
 *
 * @param {Element} element Element to find out position for.
 * @return {HPoint} Position object as point.
 */
HMapHelpers.getElementPosition = function(element) {
    var left = element.offsetLeft;
    var top = element.offsetTop;
    var parent = element.offsetParent;
    
    // bubble through hierarchy until no more parent
    while (parent != null) {
        left += parent.offsetLeft;
        top += parent.offsetTop;
        parent = parent.offsetParent;
    }
    
    return new HPoint(left, top);
};

/**
 * Get browser client size.
 *
 * @returns {Object}
 */
HMapHelpers.getClientSize = function() {
  var width = 0, height = 0;
  
  if(typeof(window.innerWidth) == 'number') {
        width = window.innerWidth;
        height = window.innerHeight;
  } else if (document.documentElement && (document.documentElement.clientWidth || document.documentElement.clientHeight)) {
        width = document.documentElement.clientWidth;
        height = document.documentElement.clientHeight;
  } else if (document.body && (document.body.clientWidth || document.body.clientHeight)) {
        width = document.body.clientWidth;
        height = document.body.clientHeight;
  }
  
  return {width: width, height: height};
}

/**
 * Apply style attributes to elements. Automatically sets
 * cssFloat/styleFloat if attribute "float" is set (for
 * cross browser support).
 *
 * @param {Element} elements Array of DOM element objects.
 * @param {Object} styles Hash of style attributes.
 */
HMapHelpers.applyStyle = function(elements, styles) {
    for (var i = 0; i < elements.length; i++) {
        for (var attr in styles) {
            if (attr == "float") {
                elements[i].style["cssFloat"] = styles[attr];
                elements[i].style["styleFloat"] = styles[attr]; // IE
            } else {
                try {
                    elements[i].style[attr] = styles[attr];
                } catch (ex) { }
            }
        }
    }
};

/**
 * Check if browser is IE based on event.
 *
 * @param {Object} event Event object.
 *
 * @returns {Boolean} True if browser detected as IE.
 */
HMapHelpers.isIEEvent = function(event) {
    var e = event || window.event;
    // preventDefault does not exist in IE but in other browsers
    return !e.preventDefault;
};

/**
 * Returns true if browser is Internet Explorer (checks
 * on existence of document.all).
 *
 * @returns {Boolean} True if browser detected as IE.
 */
HMapHelpers.isIE = function() {
    return !!document.all;
};

/**
 * Returns true if browser is Internet Explorer and version is below (not
 * including) given version (detected using user-agent).
 * 
 * @param {Number} version Version to compare with.
 * @returns {Boolean} True if IE and below given version.
 */
HMapHelpers.isIEVersionBelow = function(version) {
    var browser = navigator.appName;
    var versionStr = navigator.appVersion;

    var index = parseInt(versionStr.indexOf("MSIE")) + 1;
    var versionNumber = parseFloat(versionStr.substring(index + 4, index + 7));

    return (browser == "Microsoft Internet Explorer" && versionNumber < version);
};

/**
 * Return true if browser is Opera (detected by user agent-parsing).
 *
 * @returns {Boolean} True if browser detected as Opera.
 */
HMapHelpers.isOpera = function() {
    var browser = navigator.userAgent;

    return (browser.indexOf("Opera/") != -1);
};

/**
 * Return true if browser is Safari (detected by user agent-parsing).
 *
 * @returns {Boolean} True if browser detected as Safari.
 */
HMapHelpers.isSafari = function() {
    var browser = navigator.userAgent;

    // note that Chrome user-agent
    return (browser.indexOf("Safari/") != -1 && browser.indexOf("Chrome/") == -1);
};

/**
 * Check left mouse button click.
 *
 * @param {Object} event Event.
 * @returns {Boolean} True of left mouse button is clicked.
 */
HMapHelpers.isLeftMouseClicked = function(event) {
    var e = event || window.event;
    
    if (e.which) {
        return (e.which == 1);
    } else if (e.button) {
        return (e.button == 0 || e.button == 1);
    }
    
    // happens if clicked on map and button released outside window on IE
    return false;
};

/**
 * Check right mouse button click.
 *
 * @param {Object} event Event.
 * @returns {Boolean} True of right mouse button is clicked.
 */
HMapHelpers.isRightMouseClicked = function(event) {
    var e = event || window.event;
    
    if (e.which) {
        return (e.which == 3 || e.which == 2);
    } else if (e.button) {
        return (e.button == 2 || e.button == 3);
    }
    
    return false;
};

/**
 * Initiates a call to given URL by adding a script tag. URL is automatically
 * appended with timestamp to avoid caching in the browser.
 *
 * @param {String} url The URL to include the JSONP JavaScript from.
 * @param {Number} [timeout] After number of milliseconds to remove script tag (default 2000).
 * @param {Boolean} [cache] Set to true to prevent timestamp from being appended
 *                          to the url (default false).
 */
HMapHelpers.initJSONP = function(url, timeout, cache) {
    var timeoutArg = arguments[1] || 2000;
    var doNotAppendTimestamp = arguments[2] || false;

    if (!document.body) {
        return;
    }

    // create script tag
    var script = document.createElement("script");
    script.setAttribute("type", "text/javascript");

    // append timestamp to avoid browser caching
    if (!doNotAppendTimestamp) {
        url += (url.indexOf("?") == -1 ? "?" : "&") + (new Date().getTime());
    }
    
    // append millisecond timestamp to avoid caching
    script.setAttribute("src", url);

    var head = document.getElementsByTagName('head')[0];

    // remove script tag (use timeout to remove responsibility from callback)
    setTimeout(function() {
        head.removeChild(script);
    }, timeoutArg);

    // insert script tag
    head.appendChild(script);
};

/**
 * Dynamically adds a script tag to given URL using HMapHelpers.initJSONP. JSONP
 * callback name is appended URL with parameter "callback". Callback is reference
 * to function which is invoked when JSONP callback is triggered.
 *
 * @param {String} url The URL to include the JSONP JavaScript from.
 * @param {Function} callback Function to invoke upon callback.
 * @param {Object} that What this will refer to upon callback.
 */
HMapHelpers.getJSONP = function(url, callback, that) {
    if (!HMapHelpers.jsonCounter) {
        HMapHelpers.jsonCounter = new Date().getTime();
    } else {
        HMapHelpers.jsonCounter++;
    }

    // generate unique callback name
    var callbackName = "jsonpCallback" + HMapHelpers.jsonCounter;

    // create temporary callback function
    HMapHelpers[callbackName] = function(data) {
        callback.call(that, data);

        // free reference
        HMapHelpers[callbackName] = null;
    }

    // trigger JSONP call
    HMapHelpers.initJSONP(url + (url.indexOf("?") == -1 ? "?" : "&") + "callback=HMapHelpers." + callbackName, null, true);
};

/**
 * Set opacity of DOM element.
 *
 * @param {Element} element DOM element to change opacity of.
 * @param {Number} opacity Opacity value (0-100).
 */
HMapHelpers.setOpacity = function(element, opacity) {
    // avoid flicker when 100% in some browsers
    opacity = (opacity == 100)? 99.999 : opacity;

    // IE/Win
    element.style.filter = "alpha(opacity:" + opacity + ")";
    // Safari<1.2, Konqueror
    element.style.KHTMLOpacity = opacity/100;
    // Older Mozilla and Firefox
    element.style.MozOpacity = opacity/100;
    // Safari 1.2, newer Firefox and Mozilla, CSS3
    element.style.opacity = opacity/100;
};

/**
 * Fade DOM element by changing opacity.
 *
 * @param {Element} element DOM element to change opacity of.
 * @param {Number} from From opacity (0-100).
 * @param {Number} to To opacity (0-100).
 * @param {Number} duration Duration of fade in milliseconds.
 */
HMapHelpers.fade = function(element, from, to, duration) {
    var INTERVAL = 10;
    
    var count = Math.abs(from - to);
    var sign = (to - from) / count;
    var step = Math.ceil(sign * (count / (duration / INTERVAL)) );

    var timer = setInterval(function() {
        HMapHelpers.setOpacity(element, from);

        from += step;

        if ((sign > 0 && to <= from) || (sign < 0 && to >= from)) {
            clearInterval(timer);
        }
    }, INTERVAL);
};

/**
 * Returns the number closest to zero while keeping the sign of the first.
 *
 * @param {Number} a First number (result will have same sign as this number).
 * @param {Number} b Second number.
 * @returns {Number} Closest to zero with sign of first value.
 */
HMapHelpers.signMin = function(a, b) {
    if (a == 0 || b == 0) {
        return 0;
    }

    var absA = Math.abs(a);

    return (a / absA) * Math.min(Math.abs(b), absA);
};

/**
 * Apply IE PNG transparency fix to image object. (Only to be used with
 * Internet Explorer version below 7).
 *
 * @param {Element} element Image container object.
 * @param {String} src URL to png image.
 */
HMapHelpers.applyIEPNGFix = function(element, src) {
    element.style.filter="progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + src + "',sizingMethod='image')";
};

/**
 * Apply background image.
 * @param {Element} element Image container object.
 * @param {String} src URL to image.
 */
HMapHelpers.applyBackgroundImage = function(element, src) {
    element.style.backgroundImage="url(" + src + ")";
};

/**
 * Set background position.
 * @param {Element} element to change background position for.
 * @param {String} left offset of bg (e.g. -10px).
 * @param {String} top offset of bg (e.g. 10px).
 */
HMapHelpers.setBackgroundPosition = function(element, left, top) {
    element.style.backgroundPosition=left + " " + top;
};

/**
 * Dynamically load javascript in current document.
 *
 * @param {String} src URL to JavaScript file.
 */
HMapHelpers.loadScript = function(src) {
    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('SCRIPT');
    script.setAttribute('language', 'javascript');
    script.setAttribute('type', 'text/javascript');
    script.setAttribute('src', src);

    head.appendChild(script);
};

/**
 * Parses a string (typically document.location.hash or document.location.search)
 * and returns key=value pairs as an associative array.
 *
 * @param {String} str String to parse.
 * @returns {Object} Associative array populated by key value pairs.
 */
HMapHelpers.parseUrl = function(str) {
    if (str.indexOf('?') != -1) {
        // strip all prior to and including ?
        str = str.substring(str.indexOf('?') + 1);

        if (str.indexOf('#') > -1) {
            // strip hash
            str = str.substring(0, str.indexOf('#'));
        }
    } else if (str.indexOf('#') != -1) {
        // strip all prior to and including #
        str = str.substring(str.indexOf('#') + 1);
    }

    //var pairs = str.substring(str.indexOf('?') + 1).split('&');
    var pairs = str.split('&');

    var values = {};

    for (var i = 0; i < pairs.length; i++) {
        var pair = pairs[i].split('=');
        values[pair[0]] = pair[1];
    }

    return values;
};

/**
 * Checks if browser is other than IE or if Canvas object is available, if so
 * it returns true.
 * 
 * @returns {Boolean} True if Canvas available.
 */
HMapHelpers.isCanvas = function() {
  return (!HMapHelpers.isIE() || !(typeof window.CanvasRenderingContext2D == 'undefined' && typeof G_vmlCanvasManager == 'undefined'));
};

/**
 * Returns true if given object is an array.
 *
 * @params {Object} array Object to test.
 * @returns {Boolean} True if array.
 */
HMapHelpers.isArray = function(array) {
    return Object.prototype.toString.call(array) === '[object Array]';
};

/**
 * Forces given object to be an array if it is not.
 *
 * @params {Object} obj Object to put in array (if not already array).
 * @returns {Array} Array.
 */
HMapHelpers.makeArray = function(obj) {
    if (obj === null) return [];
    if (HMapHelpers.isArray(obj)) return obj;
    return [obj];
};
/**
 * @fileOverview Default overlay interface and implementations.
 */

/**
 * The abstract HOverlay is used to manage overlays.
 * 
 * @class Abstract class that must be implemented by all overlays.
 *
 * @property {Boolean} isAdded Is true if overlay is added to map.
 * @property {Boolean} isActive True if overlay position is automatically updated.
 * 
 * @constructor
 */
function HOverlay() {
    this.isAdded = false;     // is overlay added to map?
    this.isActive = true;     // should HMap.updateOverlays() call HOverlay.update()?
    this.container = null;    // parent container of element
}

/**
 * Starting Z-index used for overlays.
 * @constant
 * @private
 */
HOverlay.Z_INDEX_OFFSET = 10000;

/**
 * Starting Z-index used for info boxes.
 * @constant
 * @private
 */
HOverlay.Z_INDEX_OFFSET_INFO_BOX = HOverlay.Z_INDEX_OFFSET + 3000000; // (3000000=max z-index of southern most position)

/**
 * Get z-index of object located at given RT90 position. Used to make south
 * overlays appear on top of more northern objects.
 * 
 * @param {HPointRT90} rt90point The RT90 position.
 * @returns {Number} Z-index.
 * @static
 */
HOverlay.getZIndex = function(rt90point) {
    return HOverlay.Z_INDEX_OFFSET + (HMap.RT90_GEO_POINT_NORTH - rt90point.north);
};

/**
 * Initiate overlay.
 *
 * @param {HMap} hmap The HMap instance.
 */
HOverlay.prototype.init = function(hmap) {
    this.hmap = hmap;
    
    // append overlay to container
    this.container = this.hmap.getPaneOverlay();
    this.container.appendChild(this.getElement());
    
    // position overlay
    this.update();    
};

/**
 * Called when overlay is removed from the map so overlay may remove itself
 * from the parent container.
 */
HOverlay.prototype.remove = function() {
    this.container.removeChild(this.element);
};

/**
 * Called when overlay needs to update its position.
 */
HOverlay.prototype.update = function() {

};

/**
 * Get the DOM element used for the overlay.
 *
 * @returns {Element} Element used for overlay.
 */
HOverlay.prototype.getElement = function() {
    return this.element;
};

/**
 * HIcon constructor.
 * 
 * @class HIcon represents an icon used by HMarker.
 *
 * @param {String} image URL to image of the icon.
 *
 * @property {String} image URL to image of the icon.
 * @property {HPoint} anchor Pixel coordinate where the icon is anchored to
 *                    the map. Relative to the top left corner of the icon
 *                    image.
 * @property {HPoint} infoBoxAnchorTop Pixel coordinate where an info box is
 *                    anchored to the icon when the info box is above the icon.
 *                    Relative to the top left corner of the icon image.
 * @property {HPoint} infoBoxAnchorBottom Pixel coordinate where an info box is
 *                    anchored to the icon when the info box is under the icon.
 *                    Relative to the top left corner of the icon image.
 * @property {HSize} size Width and height of the icon image.
 * @property {int} iconIndex If the image specified is a sprite, iconIndex
 *                    specifies the horizontal offset of the prite (offset * size.width).
 * 
 * @constructor
 */
function HIcon(image) {
    this.image = image;
    this.anchor = new HPoint(0, 0);
    this.infoBoxAnchorTop = new HPoint(0, 0);
    this.infoBoxAnchorBottom = new HPoint(0, 0);
    this.size = null;
    this.iconIndex = null;
}

/**
 * Default Hitta.se icon
 */
HIcon.H_DEFAULT_ICON = {
    image: "http://www.hitta.se/images/point.png",
    anchor: new HPoint(10, 11),
    infoBoxAnchorTop: new HPoint(10, -2),
    infoBoxAnchorBottom: new HPoint(10, 25),
    size: new HSize(25, 25),
    iconIndex: null
};

/**
 * HMarker constructor.
 * 
 * @class HMarker represents a marker that may be positioned
 * on a given position on the map.
 *
 * @param {HPointRT90} rt90point RT90 coordinate where marker is positioned.
 * @param {HIcon} [icon] Object specifying icon used for the marker.
 *
 * @augments HOverlay
 * @constructor
 */
function HMarker(rt90point, icon) {
    var me = this;

    me.icon = arguments[1] || HIcon.H_DEFAULT_ICON;

    me.mode = HMarker.MODE_INFOBOX_CLICK;

    me.rt90point = rt90point;
    me.draggingEnabled = false;     // dragging disabled by default
    me.eventListeningEnabled = true;  // event listening on by default
    me.overrideZIndex = null;

    me.isMarker = true;

    if(me.icon.iconIndex != null){ //sprite
        me.element = document.createElement("div");
        HMapHelpers.applyStyle([me.element], {position: "absolute", display: "block", overflow: "hidden"});

        if (HMapHelpers.isIEVersionBelow(7)) {
            me.imgElement = document.createElement("div");
            HMapHelpers.applyStyle([me.imgElement], {position: "relative", display: "block"});
            me.element.appendChild(me.imgElement);
        }
    }else{ //non prite
        me.element = document.createElement("img");
        HMapHelpers.applyStyle([me.element], {
        position: "absolute"
    });
    }

    this.setIcon(me.icon);
}

// HMarker inherits from HOverlay.
HMarker.prototype = new HOverlay();

/**
 * If mode set to infobox click the assoicated infobox is opened when clicking
 * on the marker and closed when clicked again. This is the default mode.
 *
 * @constant
 * @see HMarker#setMode
 */
HMarker.MODE_INFOBOX_CLICK = 0;

/**
 * If mode set to infobox hover the associated infobox will be opened on
 * mouse over event of the marker and be kept open until the mouse is not
 * positioned on the marker or the infobox. The closing has a delay.
 *
 * @constant
 * @see HMarker#setMode
 */
HMarker.MODE_INFOBOX_HOVER = 1;

/**
 * Delay in milliseconds until the infobox will close after mouse has left
 * marker and is not positioned on the infobox itself. Used in infobox hover
 * mode.
 *
 * @constant
 * @private
 */
HMarker.INFOBOX_CLOSE_DELAY = 1100;

/**
 * Determines how close to the border in pixels the mouse pointer has to be
 * when dragging a marker to cause the map to automatically scroll.
 *
 * @constant
 * @private
 */
HMarker.AUTO_SCROLL_ZONE = 20;

/**
 * Auto scroll speed in pixels per update.
 * 
 * @constant
 * @private
 */
HMarker.AUTO_SCROLL_SPEED = 2;

/**
 * Event triggered when dragging of markers has started.
 *
 * @event
 * @param {HPointRT90} rt90point Current RT90 position of the marker.
 */
HMarker.EVENT_DRAG_START = "markerdragstart";

/**
 * Event triggered when dragging of markers has ended.
 *
 * @event
 * @param {HPointRT90} rt90point Current RT90 position of the marker.
 */
HMarker.EVENT_DRAG_END = "markerdragend";

/**
 * Event triggered when marker is dragged.
 *
 * @event
 * @param {HPointRT90} rt90point Current RT90 position of the marker.
 */
HMarker.EVENT_DRAG_MOVE = "markerdragmove";

/**
 * Event triggered when mouse button is released on marker.
 *
 * @event
 * @param {HPointRT90} rt90point Current RT90 position of the marker.
 */
HMarker.EVENT_MOUSE_UP = "markermouseup";

/**
 * Event triggered when mouse button is pressed down on marker.
 *
 * @event
 * @param {HPointRT90} rt90point Current RT90 position of the marker.
 */
HMarker.EVENT_MOUSE_DOWN = "markermousedown";

/**
 * Event triggered when marker is clicked on.
 * 
 * @event
 * @param {HPointRT90} rt90point Current RT90 position of the marker.
 */
HMarker.EVENT_MOUSE_CLICK = "markermouseclick";

/**
 * Event triggered when marker is double clicked on.
 *
 * @event
 * @param {HPointRT90} rt90point Current RT90 position of the marker.
 */
HMarker.EVENT_MOUSE_DBLCLICK = "markermousedblclick";

/**
 * Event triggered when mouse is over the marker.
 *
 * @event
 * @param {HPointRT90} rt90point Current RT90 position of the marker.
 */
HMarker.EVENT_MOUSE_OVER = "markermouseover";

/**
 * Event triggered when mouse is leaving the marker.
 *
 * @event
 * @param {HPointRT90} rt90point Current RT90 position of the marker.
 */
HMarker.EVENT_MOUSE_OUT = "markermouseout";

/**
 * Event triggered when infobox associated with marker is opened.
 *
 * @event
 */
HMarker.EVENT_INFOBOX_OPEN = "markerinfoboxopen";

/**
 * Event triggered when infobox associated with marker is closed.
 *
 * @event
 */
HMarker.EVENT_INFOBOX_CLOSE = "markerinfoboxclose";

/**
 * Initiate marker box. Called when added to container.
 *
 * @param {HMap} hmap The HMap instance.
 * @private
 */
HMarker.prototype.init = function(hmap) {
    var me = this;
    me.hmap = hmap;

    // append info box to container
    this.container = me.hmap.getPaneOverlay();
    this.container.appendChild(this.getElement());

    // position overlay
    me.update();

    // register event listeners
    if (this.isEventListening()) {
        this.registerEventListeners();
    }
};

/**
 * Called when marker is removed from map. Closes info box if attached.
 *
 * @private
 */
HMarker.prototype.remove = function() {
    HOverlay.prototype.remove.call(this);

    this.unregisterEventListeners();

    this.closeInfoBox();
};

/**
 * Enable dragging of the marker on the map (disabled by default).
 */
HMarker.prototype.enableDragging = function() {
    this.draggingEnabled = true;
};

/**
 * Disables dragging of the marker on the map.
 */
HMarker.prototype.disableDragging = function() {
    this.draggingEnabled = false;
};

/**
 * Returns true if dragging of the marker on the map is enabled.
 *
 * @returns {Boolean} True if dragging is enabled.
 */
HMarker.prototype.isDraggingEnabled = function() {
    return this.draggingEnabled;
};

/**
 * Enable event listening of the marker (enabled by default). Disable this and
 * the marker will not listen to clicks, mouse over/out, etc.
 */
HMarker.prototype.enableEventListening = function() {
    this.registerEventListeners();
};

/**
 * Disables event listening of the marker.
 */
HMarker.prototype.disableEventListening = function() {
    this.unregisterEventListeners();
};

/**
 * Returns true if event listening of the marker is enabled.
 *
 * @returns {Boolean} True if event listening is enabled.
 */
HMarker.prototype.isEventListening = function() {
    return this.eventListeningEnabled;
};

/**
 * Register event listeners for the marker.
 * 
 * @private
 */
HMarker.prototype.registerEventListeners = function() {
    if (!this.listeners) {
        this.listeners = [];

        var me = this;

        this.listeners.push( HEvent.addDomListener(me.element, "click", function(e) {return me.click(e);}) );
        this.listeners.push( HEvent.addDomListener(me.element, "dblclick", function(e) {return me.dblClick(e);}) );

        // attach mouse events to allow dragging
        this.listeners.push( HEvent.addDomListener(me.element, "mousedown", function(e) {return me.mouseDown(e);}) );
        this.listeners.push( HEvent.addDomListener(document, "mouseup", function(e) {return me.stopDrag(e);}) );
        this.listeners.push( HEvent.addDomListener(document, "mousemove", function(e) {return me.dragging(e);}) );

        this.listeners.push( HEvent.addDomListener(me.element, "mouseup", function(e) {return me.mouseUp(e);}) );
        this.listeners.push( HEvent.addDomListener(me.element, "mouseover", function(e) {return me.mouseOver(e);}) );
        this.listeners.push( HEvent.addDomListener(me.element, "mouseout", function(e) {return me.mouseOut(e);}) );

        this.eventListeningEnabled = true;

        HMapHelpers.applyStyle([this.element], {
            cursor: "pointer"
        });
    }
};

/**
 * Unregister any event listeners associated to the marker.
 *
 * @private
 */
HMarker.prototype.unregisterEventListeners = function() {
    this.eventListeningEnabled = false;

    HMapHelpers.applyStyle([this.element], {
        cursor: ""
    });

    if (this.listeners) {
        HEvent.removeListeners(this.listeners);
        this.listeners = null;
    }
};

/**
 * Get RT90 position of marker.
 *
 * @returns {HPointRT90} RT90 position of marker.
 */
HMarker.prototype.getRT90Point = function() {
    return this.rt90point;
};

/**
 * Set RT90 position of marker (will trigger update).
 *
 * @param {HPointRT90} rt90point RT90 position to move marker to.
 */
HMarker.prototype.setRT90Point = function(rt90point /*, skipUpdate */) {
    var skipUpdate = !!arguments[1];

    this.rt90point = new HPointRT90(rt90point.north, rt90point.east);
    if (!skipUpdate) {
        this.update();
    }
};

/**
 * Get container pixel point of marker anchor.
 *
 * @returns {HPoint} Container pixel coordinates.
 */
HMarker.prototype.getContainerPixelPoint = function() {
    return new HPoint(this.point.x + this.icon.anchor.x, this.point.y + this.icon.anchor.y);
};

/**
 * Get icon of marker.
 *
 * @returns {HIcon} Icon object of marker.
 */
HMarker.prototype.getIcon = function() {
    return this.icon;
};

/**
 * Set new icon of marker. Will trigger an update to reposition the marker
 * since the icon may be of another size. In order to just change the image
 * with another image of the same size, use HMarker.setImage.
 *
 * @param {HIcon} icon Icon object.
 * @see HMarker#setImage
 */
HMarker.prototype.setIcon = function(icon) {
    this.icon = icon;

    var size = this.icon.size ? this.icon.size : null;

    this.setImage(this.icon.image, size, this.icon.iconIndex);

    if (this.hmap) {
        this.update();
    }
};

/**
 * Get DOM element of marker image.
 * 
 * @returns {Element} DOM element of marker image.
 */
HMarker.prototype.getElement = function() {
    return this.element;
};

/**
 * Set marker image. Note that this will not change the icon settings, thus the
 * image needs to be the same size as the current. This is mainly intended to
 * provide functionality like highlighting an marker when mouse is over.
 *
 * @param {String} url URL to image.
 * @param {HSize} [size] Apply width and height to the image.
 * @param {int} [iconIndex] If a sprite is use, this is the horizontal index.
 */
HMarker.prototype.setImage = function(url, size, iconIndex) {


    var imgIndex = arguments[2] == undefined ? null : arguments[2];
    var imgSize = arguments[1] || null;

    if(imgIndex != null){ //sprite
        if (HMapHelpers.isIEVersionBelow(7)) {
            // png fix for IE < v7
            HMapHelpers.applyIEPNGFix(this.imgElement, url);
        } else {
            HMapHelpers.applyBackgroundImage(this.element, url);
        }

        if (imgSize && imgSize.width && imgSize.height) {
            HMapHelpers.applyStyle([this.element], {
                height: imgSize.height + "px",
                width: imgSize.width + "px"
            });
            if (HMapHelpers.isIEVersionBelow(7)) {
                HMapHelpers.applyStyle([this.imgElement], {
                    height: imgSize.height + "px",
                    width: imgSize.width + "px"
                });
            }

            var spriteOffset = imgSize.width * -imgIndex;

            if(HMapHelpers.isIEVersionBelow(7)){
                HMapHelpers.applyStyle([this.imgElement], {left: spriteOffset + "px"});
            }else{
                HMapHelpers.setBackgroundPosition(this.element, spriteOffset + "px", "0px")
            }
        }
    }else{ //non sprite
        if (HMapHelpers.isIEVersionBelow(7)) {
            // png fix for IE < v7
            HMapHelpers.applyIEPNGFix(this.element, url);
        } else {
            this.element.src = url;
        }

        if (imgSize && imgSize.width && imgSize.height) {
            HMapHelpers.applyStyle([this.element], {
                height: imgSize.height + "px",
                width: imgSize.width + "px"
            });
        }
    }
};

/**
 * Set marker z-index. This will override the default z-index behavior of more
 * southern markers getting higher z-index, and instead setting the given 
 * z-index permanently.
 * 
 * @param {Number} index Permanent Z-index.
 */
HMarker.prototype.setZIndex = function(index) {
    this.overrideZIndex = index;
    this.update();
};

/**
 * Set infobox behavior mode to <code>HMarker.MODE_INFOBOX_CLICK</code> or
 * <code>HMarker.MODE_INFOBOX_HOVER</a>.
 * 
 * @param {Number} mode Mode to set.
 * @see <a href="#.MODE_INFOBOX_CLICK">HMarker.MODE_INFOBOX_CLICK</a>
 * @see <a href="#.MODE_INFOBOX_HOVER">HMarker.MODE_INFOBOX_HOVER</a>
 */
HMarker.prototype.setMode = function(mode) {
    this.mode = mode;
};

/**
 * Hide marker.
 */
HMarker.prototype.hide = function() {
    this.element.style.display = "none";
};

/**
 * Show marker.
 */
HMarker.prototype.show = function() {
    this.element.style.display = "block";
};

/**
 * Returns true if display property is set to hide marker.
 */
HMarker.prototype.isHidden = function() {
    return (this.element.style.display == "none");
};

/**
 * Call to trigger recalculation of position for marker.
 *
 * @private
 */
HMarker.prototype.update = function() {
    this.point = this.hmap.fromRT90toContainerPixel(this.rt90point);
    
    // offset by anchor position
    this.point.x -= this.icon.anchor.x;
    this.point.y -= this.icon.anchor.y;

    var zIndex = this.overrideZIndex;
    if (zIndex === null) {
        zIndex = HOverlay.getZIndex(this.rt90point);
    }

    HMapHelpers.applyStyle([this.element], {
        top: this.point.y + "px",
        left: this.point.x + "px",
        zIndex: zIndex
    });

    if (this.isInfoBoxOpen()) {
        this.infoBox.update();
        this.infoBox.mapMove();
    }
};

/**
 * Pan marker.
 *
 * @param {Number} dx Delta X movement in pixels to pan.
 * @param {Number} dy Delta Y movement in pixels to pan.
 *
 * @private
 */
HMarker.prototype.pan = function(dx, dy) {
    this.point.x += dx;
    this.point.y += dy;
    
    this.element.style.left = this.point.x + "px";
    this.element.style.top = this.point.y + "px";
};

/**
 * Open info box and bind it to the marker. Set content and title to null to remove
 * any currently bound infobox.
 * 
 * @param {String} content Info box content as HTML text.
 * @param {HSize} size Size of the info box.
 * @param {HIcon} [icon] Icon to be used in info box.
 * @param {String} [title] The title to use for the infobox.
 */
HMarker.prototype.openInfoBoxHtml = function(content, size, icon, title) {
    if (content || title) {
        this.closeInfoBox();
        this.infoBox = new HInfoBox(this, size, HInfoBox.ANCHOR_BOTTOMLEFT, content, icon, title);
        this._openInfoBox();
    } else {
        this.infoBox = null;
    }
};

/**
 * Open info box bound to marker.
 */
HMarker.prototype.openInfoBoxBound = function() {
    this._openInfoBox();
};

/**
 * Open attached info box. If marked has a category set, then info box will
 * get the same category.
 *
 * @private
 */
HMarker.prototype._openInfoBox = function() {
    if (this.infoBox && !this.infoBox.isAdded) {
        if (this.hmap.isExclusiveInfoBoxEnabled()) {
            this.hmap.closeInfoBoxes();
        }

        HEvent.trigger(this, HMarker.EVENT_INFOBOX_OPEN);

        this.hmap.addOverlay(this.infoBox, this.category);
    }
};

/**
 * Bind an info box to the marker. It will be automatically displayed upon click
 * on the marker. Set content to null to unbind the info box from the marker. The
 * info box element is permanently added to the DOM first when opened.
 *
 * @param {String} content Info box content as HTML text.
 * @param {HSize} size Size of the info box.
 */
HMarker.prototype.bindInfoBoxHtml = function(content, size) {
    if (content !== null) {
        this.infoBox = new HInfoBox(this, size, HInfoBox.ANCHOR_BOTTOMLEFT, content);
    } else {
        this.infoBox = null;
    }
};

/**
 * Close info box attached to marker.
 */
HMarker.prototype.closeInfoBox = function() {
    if (this.isInfoBoxOpen()) {
        HEvent.trigger(this, HMarker.EVENT_INFOBOX_CLOSE);
        
        this.hmap.removeOverlay(this.infoBox);
    }
};

/**
 * Returns true if an info box is attached to the marker and open.
 *
 * @returns {Boolean} True if attached info box is open.
 */
HMarker.prototype.isInfoBoxOpen = function() {
    return (this.infoBox && this.infoBox.isAdded);
};

/**
 * Returns the info box attached to the marker or null if no
 * info box is attached.
 * 
 * @returns {HInfoBox} Info box reference or null if no info box attached.
 */
HMarker.prototype.getInfoBox = function() {
    return this.infoBox;
};

/**
 * Event handler for mouse click event on marker.
 *
 * @param {Object} e Event.
 * @private
 */
HMarker.prototype.click = function(e) {
    if (!this.isDragging) {
        HEvent.trigger(this, HMarker.EVENT_MOUSE_CLICK, this.getRT90Point());
        HEvent.trigger(this.hmap, HMap.EVENT_MARKER_CLICK, this);

        // if info box attached, automatically toggle open/close state
        if (this.mode == HMarker.MODE_INFOBOX_CLICK) {
            if (this.isInfoBoxOpen()) {
                this.closeInfoBox();
            } else {
                this._openInfoBox();
            }
        } else if (this.mode == HMarker.MODE_INFOBOX_HOVER) {
            if (this.isInfoBoxOpen()) {
                this.closeInfoBox();
            }
        }

        return HEvent.stopEvent(e);
    }

    return true;
};

/**
 * Event handler for mouse double click event on marker.
 *
 * @param {Object} e Event.
 * @returns {Boolean} Always returns false.
 * @private
 */
HMarker.prototype.dblClick = function(e) {
    HEvent.trigger(this, HMarker.EVENT_MOUSE_DBLCLICK, this.getRT90Point());
    HEvent.trigger(this.hmap, HMap.EVENT_MARKER_DBLCLICK, this);

    return this.mode == HMarker.MODE_INFOBOX_CLICK ? HEvent.stopEvent(e) : true;
};

/**
 * Mouse down event handler.
 * 
 * @param {Event} e Mouse event.
 */
HMarker.prototype.mouseDown = function(e) {
    HEvent.trigger(this, HMarker.EVENT_MOUSE_DOWN, this.getRT90Point());
    
    if (this.draggingEnabled) {
        return this.startDrag(e);
    }

    return true;
};

/**
 * Mouse up event handler.
 *
 * @param {Event} e Mouse event.
 */
HMarker.prototype.mouseUp = function(e) {
    HEvent.trigger(this, HMarker.EVENT_MOUSE_UP, this.getRT90Point());
};

/**
 * Mouse out event handler.
 *
 * @param {Event} e Mouse event.
 */
HMarker.prototype.mouseOut = function(e) {
    if (this.mode == HMarker.MODE_INFOBOX_HOVER && this.isInfoBoxOpen()) {
        var me = this;
        me._infoboxElement = me.getInfoBox().getElement();
        
        setTimeout(function() {
            if (!me.getInfoBox().mouseIsOver && !me.mouseIsOver) {
                me.closeInfoBox();
                if (me.infoboxOutListener) {
                    HEvent.removeListener(me.infoboxOutListener);
                }
                me.infoboxOutListener = null;
            } else {
                if (!me.infoboxOutListener) {
                    me.infoboxOutListener = HEvent.addDomListener(me._infoboxElement, "mouseout", function(event) {
                        // this code is to handle mouseout events from child objects
                        var e = event || window.event;
                        var elementEntering = (e.relatedTarget) ? e.relatedTarget : e.toElement;
                        // check if parent is not the element itself
                        while (elementEntering != me._infoboxElement && elementEntering && elementEntering.nodeName != 'BODY')
                            elementEntering = elementEntering.parentNode;
                        if (elementEntering == me._infoboxElement) return;

                        setTimeout(function() {
                            if (!me.getInfoBox().mouseIsOver && !me.mouseIsOver) {
                                me.closeInfoBox();
                                if (me.infoboxOutListener) {
                                    HEvent.removeListener(me.infoboxOutListener);
                                }
                                me.infoboxOutListener = null;
                            }
                        }, HMarker.INFOBOX_CLOSE_DELAY);
                    });
                }
            }
        }, HMarker.INFOBOX_CLOSE_DELAY);
    }

    this.mouseIsOver = false;

    HEvent.trigger(this, HMarker.EVENT_MOUSE_OUT, this.getRT90Point());
};

/**
 * Mouse over event handler.
 *
 * @param {Event} e Mouse event.
 */
HMarker.prototype.mouseOver = function(e) {
    HEvent.trigger(this, HMarker.EVENT_MOUSE_OVER, this.getRT90Point());

    if (this.mode == HMarker.MODE_INFOBOX_HOVER) {
        this._openInfoBox();
    }

    this.mouseIsOver = true;
};

/**
 * Start dragging.
 *
 * @param {Event} e Mouse event.
 * @return {Boolean} False to stop event bubbling when dragging is enabled.
 * @private
 */
HMarker.prototype.startDrag = function(e) {
    if (this.draggingEnabled) {
        var pos = HMapHelpers.getMousePosition(e);

        this.dragStart = pos;
        this.dragMarkerStart = new HPoint(this.element.offsetLeft, this.element.offsetTop);

        // note that the start drag event is triggered in dragging() first when
        // the marker has actually moved

        return HEvent.stopEvent(e);
    }
    
    return true;
};

/**
 * Event handler for mouse up event on marker.
 *
 * @param {Object} e Event.
 * @return {Boolean} False if dragging to stop event bubbling.
 * @private
 */
HMarker.prototype.stopDrag = function(e) {

    var stopEventBubbling = false;

    if (this.isDragging) {
        // delay setting dragging state to false to allow click event handler
        // to check for dragging (to prevent click to register on mouseup when
        // dragging stopped)
        var me = this;
        setTimeout(function() {me.isDragging = false;}, 100);

        // trigger update to update this.point
        this.update();

        HEvent.trigger(this, HMarker.EVENT_DRAG_END, this.getRT90Point());

        HEvent.stopEvent(e);
        stopEventBubbling = true;
    }

    this.dragStart = null;

    return !stopEventBubbling;
};

/**
 * Perform dragging of marker.
 *
 * @param {Event} e Mouse event.
 * @return {Boolean} False if dragging to stop event bubbling.
 * @private
 */
HMarker.prototype.dragging = function(e) {
    if (this.draggingEnabled) {
        var pos = HMapHelpers.getMousePosition(e);

        if (this.dragStart != null && !this.dragStart.equals(pos)) {

            var elementLeft = this.dragMarkerStart.x + (pos.x - this.dragStart.x);
            var elementTop = this.dragMarkerStart.y + (pos.y - this.dragStart.y);

            var G = this.hmap.globals;

            // get mouse position in container pixels
            var mousePos = new HPoint(pos.x - G.mapPosition.x, pos.y - G.mapPosition.y);

            // auto scroll when near edge
            if (mousePos.y < G.paddingTop + HMarker.AUTO_SCROLL_ZONE) {
                this.hmap.pan(0, HMarker.AUTO_SCROLL_SPEED);
            } else if (mousePos.y > G.mapSize.height - G.paddingBottom - HMarker.AUTO_SCROLL_ZONE) {
                this.hmap.pan(0, -HMarker.AUTO_SCROLL_SPEED);
            }

            if (mousePos.x < G.paddingLeft + HMarker.AUTO_SCROLL_ZONE) {
                this.hmap.pan(HMarker.AUTO_SCROLL_SPEED, 0);
            } else if (mousePos.x > G.mapSize.width - G.paddingRight - HMarker.AUTO_SCROLL_ZONE) {
                this.hmap.pan(-HMarker.AUTO_SCROLL_SPEED, 0);
            }

            // get new RT90 coordinate where the marker is dragged to
            var rt90point = this.hmap.fromContainerPixelToRT90(
                new HPoint(
                    elementLeft + this.icon.anchor.x,
                    elementTop + this.icon.anchor.y
                )
            );

            // set to RT90 point pixel (may be slightly different due to rounding)
            this.setRT90Point(rt90point, true);

            if (!this.isDragging) {
                this.isDragging = true;
                HEvent.trigger(this, HMarker.EVENT_DRAG_START, rt90point);
            }

            HEvent.trigger(this, HMarker.EVENT_DRAG_MOVE, rt90point);

            this.element.style.left = elementLeft + "px";
            this.element.style.top = elementTop + "px";

            if (this.isInfoBoxOpen()) {
                this.infoBox.update();
            }

            return HEvent.stopEvent(e);
        }
    }

    return true;
};

/**
 * HInfoBox constructor. The size object specifies the size of the content area
 * of the infobox (the total size is slightly larger due to the borders and the
 * tip). If size is null the size is estimated based on the content. The HSize
 * object may have any of its properties (width and height) null, then only that
 * property will be estimated while the other is fixed. Exactly how the size is
 * estimated is browser dependent.
 *
 * Note that estimating size incurrs a significant performance penalty, so use
 * wisely.
 *
 * @class Infobox displaying information typically associated with a marker.
 *
 * @param {HPoint|HMarker} anchor Where to anchor the info box, either
 *                         container pixel coordinates or marker to attach to.
 * @param {HSize|null} size Desired inner size of info box. If null size is
 *                     estimated based on content.
 * @param {Number} anchorType Type of anchor.
 * @param {String} content Content of info box as HTML.
 *
 * @augments HOverlay
 * @constructor
 */
function HInfoBox(anchor, size, anchorType, content, icon, title) {
    //console.log("HInfoBox", anchor, size, anchorType, content);

    var headerIcon = arguments[4] || null;
    var headerTitle = arguments[5] || null;

    if (anchor.x && anchor.y) {
        // anchor is HPoint
        this.setPoint(anchor);
    } else {
        // anchor is HMarker
        this.attachToMarker(anchor);
    }

    this.anchorType = anchorType;
    this.size = size;
    this.zIndex = HOverlay.Z_INDEX_OFFSET_INFO_BOX;
    
    this.sizeApplied = false;

    // content element
    var divInfoboxContent = document.createElement("div");
    divInfoboxContent.innerHTML = content;

    if(headerIcon && headerTitle)
    {
        //Create header
        this.headerDiv = document.createElement("Div");
        HMapHelpers.applyStyle([this.headerDiv], {display: "block",
                                        position: "relative",
                                        width:  100 + "%",
                                        minHeight: icon.size.height + "px",
                                        marginBottom: "10px"});
                                    
        //Create headerIconDiv
        this.iconDiv = HMapHelpers.createIconElement(headerIcon);
        HMapHelpers.applyStyle([this.iconDiv], {position: "absolute",
                                        top:  0 + "px",
                                        left: 0 + "px"});
                                    
        //Create headerTitleDiv
        this.labelDiv = document.createElement("Div");
        HMapHelpers.applyStyle([this.labelDiv], {position: "relative",
                                        width: size.width - icon.size.width - 15 + "px",
                                        left: icon.size.width + 10 + "px"});
                                    
        var textSpan = document.createElement("span");
        HMapHelpers.applyStyle([textSpan], {fontWeight: "bold"});

        textSpan.innerHTML = title;
        
        this.labelDiv.appendChild(textSpan);

        this.headerDiv.appendChild(this.iconDiv);
        this.headerDiv.appendChild(this.labelDiv);
        
        var compositContent = document.createElement("Div");
        HMapHelpers.applyStyle([compositContent], {position: "absolute"});

        compositContent.appendChild(this.headerDiv);
        compositContent.appendChild(divInfoboxContent);
        
        // save reference to content element and main infobox element
        this.contentElement = compositContent;
    }
    else
    {
        // save reference to content element and main infobox element
        this.contentElement = divInfoboxContent;
    }

    

    
}

// HInfoBox inherits from HOverlay.
HInfoBox.prototype = new HOverlay();

HInfoBox.CFG_POSITION_OFFSET_TOP = 0; //14;
HInfoBox.CFG_POSITION_OFFSET_BOTTOM = 0; //46;
HInfoBox.CFG_POSITION_OFFSET_LEFT = 0; //62;

// Anchor type enums (do not alter constant values, +/-2 switches between top/bottom)
HInfoBox.ANCHOR_NONE = 0;
HInfoBox.ANCHOR_TOPLEFT = 1;
HInfoBox.ANCHOR_TOPRIGHT = 2;
HInfoBox.ANCHOR_BOTTOMLEFT = 3;
HInfoBox.ANCHOR_BOTTOMRIGHT = 4;

HInfoBox.POINTER_WIDTH = 27;
HInfoBox.POINTER_HEIGHT = 14;

HInfoBox.POINTER_INDENT = 15;

HInfoBox.FLIP_OFFSET_HORZ = HInfoBox.POINTER_INDENT + parseInt(HInfoBox.POINTER_WIDTH/2);
HInfoBox.FLIP_OFFSET_VERT = HInfoBox.POINTER_HEIGHT + 50; // 50 = gap to avoid re-flip on same position

HInfoBox.POINTER_IMG_TOP = 'http://www.hitta.se/images/hmap/infobox_pointer_top.gif';
HInfoBox.POINTER_IMG_BOTTOM = 'http://www.hitta.se/images/hmap/infobox_pointer_bottom.gif';

HInfoBox.prototype.setStyle = function() {

    HMapHelpers.applyStyle([this.contentElement], {
        border: "1px solid #000",
        backgroundColor: "#fff",
        margin: "0px",
        position: "absolute",
        fontFamily: "verdana",
        fontSize: "11px",
        padding: "5px",
        zIndex: (this.zIndex + 2),
        MozBorderRadius: "7px",
        WebkitBorderRadius: "7px",
        MozBoxShadow: "3px 3px 5px #333",
        WebkitBoxShadow: "3px 3px 5px #333"
    });

    //alert(arguments.callee.caller.toString());
}

/**
* Construct info box elements.
* 
* @private
*/
HInfoBox.prototype.constructInfoBox = function() {
    //console.log("constructInfoBox");
    var divInfoboxContent = this.contentElement;
    /*
    HMapHelpers.applyStyle([divInfoboxContent], {
    border: "1px solid #000",
    backgroundColor: "#fff",
    margin: "0px",
    position: "absolute",
    fontFamily: "verdana",
    fontSize: "11px",
    padding: "5px",
    zIndex: (this.zIndex + 2),
    MozBorderRadius: "7px",
    WebkitBorderRadius: "7px",
    MozBoxShadow: "3px 3px 5px #333",
    WebkitBoxShadow: "3px 3px 5px #333"
    });*/

    this.setStyle();

    // Create and apply attributes
    var divPointerBottomRight = document.createElement("div");
    var divPointerTopLeft = document.createElement("div");
    var divPointerTopRight = document.createElement("div");
    var divPointerBottomLeft = document.createElement("div");

    HMapHelpers.applyStyle([
            divPointerBottomRight,
            divPointerTopLeft,
            divPointerTopRight,
            divPointerBottomLeft,
        ], {
      margin: "0px",
      padding: "0px",
      overflow: "hidden",
      zIndex: this.zIndex,
      position: "absolute",
      backgroundRepeat: "no-repeat"
    });

    HMapHelpers.applyStyle([divPointerBottomRight], {
      bottom: -HInfoBox.POINTER_HEIGHT + "px",
      right: HInfoBox.POINTER_INDENT + "px",
      width: HInfoBox.POINTER_WIDTH + "px",
      height: HInfoBox.POINTER_HEIGHT + "px",
      backgroundImage: "url('" + HInfoBox.POINTER_IMG_BOTTOM + "')"
    });

    HMapHelpers.applyStyle([divPointerBottomLeft], {
      bottom: -HInfoBox.POINTER_HEIGHT + "px",
      left: HInfoBox.POINTER_INDENT + "px",
      width: HInfoBox.POINTER_WIDTH + "px",
      height: HInfoBox.POINTER_HEIGHT + "px",
      backgroundImage: "url('" + HInfoBox.POINTER_IMG_BOTTOM + "')"
    });

    HMapHelpers.applyStyle([divPointerTopLeft], {
      top: -HInfoBox.POINTER_HEIGHT + "px",
      left: HInfoBox.POINTER_INDENT + "px",
      width: HInfoBox.POINTER_WIDTH + "px",
      height: HInfoBox.POINTER_HEIGHT + "px",
      backgroundImage: "url('" + HInfoBox.POINTER_IMG_TOP + "')"
    });

    HMapHelpers.applyStyle([divPointerTopRight], {
      top: -HInfoBox.POINTER_HEIGHT + "px",
      right: HInfoBox.POINTER_INDENT + "px",
      width: HInfoBox.POINTER_WIDTH + "px",
      height: HInfoBox.POINTER_HEIGHT + "px",
      backgroundImage: "url('" + HInfoBox.POINTER_IMG_TOP + "')"
    });

    this.pointerBottomRight = divPointerBottomRight;
    this.pointerTopLeft = divPointerTopLeft;
    this.pointerTopRight = divPointerTopRight;
    this.pointerBottomLeft = divPointerBottomLeft;

    this.element = divInfoboxContent;
    this.pointer = null;

    // set pointer position
    this.setAnchorType(this.anchorType);
};


/**
 * Initiate info box. Called when added to container.
 *
 * @param {HMap} hmap The HMap instance.
 * @private
 */
HInfoBox.prototype.init = function(hmap) {
    var me = this;
    me.hmap = hmap;

    var elem = this.getElement();

    if (elem == null) {
        this.constructInfoBox();
        elem = this.getElement();
    }

    // initially render box out of view to avoid flicker
    HMapHelpers.applyStyle([elem], {
        top: "-1000px",
        left: "-1000px"
    });

    // append info box to container
    this.container = this.hmap.getPaneOverlay();
    this.container.appendChild(elem);

    //console.log("init 1", this.size, elem.offsetWidth, elem.offsetHeight);

    if (!this.sizeApplied) {
        if (this.size == null) {
            this.size = new HSize(elem.offsetWidth, elem.offsetHeight);
        } else {
            this.setSize(this.size);
        }
    }
    this.sizeApplied = true;

    //Adjust title label offset
    if(me.iconDiv && me.labelDiv){
        var offset = (me.iconDiv.offsetHeight - me.labelDiv.offsetHeight) / 2;

        if(offset > 0){
            HMapHelpers.applyStyle([me.labelDiv], {top: offset + "px"});
        }
    }

    //console.log("init 2", this.size, elem.offsetWidth, elem.offsetHeight);

    // position overlay
    this.update();
    this.mapMove();

    this.listeners = [];

    this.listeners.push( HEvent.addListener(this.hmap, HMap.EVENT_INTERNAL_MAP_MOVE, function() {me.mapMove();}) );
    this.listeners.push( HEvent.addDomListener(this.element, "mousemove", HEvent.stopEvent) );
    this.listeners.push( HEvent.addDomListener(this.element, "mouseover", function(e) {me.mouseOver();}) );
    this.listeners.push( HEvent.addDomListener(this.element, "mouseout", function(e) {me.mouseOut();}) );

    HEvent.trigger(me, HMap.EVENT_INFO_BOX_OPEN, this);
};

/**
 * Explicitly set size of info box.
 * 
 * @param {HSize} size Desired size (may have null properties)
 */
HInfoBox.prototype.setSize = function(size) {
    var attr = {};
    
    if (size.width != null) {
        attr.width = size.width + 'px';
    }

    if (size.height != null) {
        attr.height = size.height + 'px';
    }

    //console.log("setSize", attr);

    // set element style
    HMapHelpers.applyStyle([this.getElement()], attr);

    // determine resulting style and save it
    this.size = new HSize(this.getElement().offsetWidth, this.getElement().offsetHeight);
};

/**
 * Set anchor type.
 *
 * @param {Number} anchorType Anchor type constant.
 * @private
 */
HInfoBox.prototype.setAnchorType = function(anchorType) {
    this.anchorType = anchorType;

    // remove existing pointer
    if (this.pointer != null) {
        this.element.removeChild(this.pointer);
    }
    
    // position anchor pointer
    switch (anchorType) {
        case HInfoBox.ANCHOR_TOPLEFT:
            this.pointer = this.pointerTopLeft;
            break;
        case HInfoBox.ANCHOR_TOPRIGHT:
            this.pointer = this.pointerTopRight;
            break;
        case HInfoBox.ANCHOR_BOTTOMLEFT:
            this.pointer = this.pointerBottomLeft;
            break;
        case HInfoBox.ANCHOR_BOTTOMRIGHT:
            this.pointer = this.pointerBottomRight;
            break;
    }

    if (this.pointer != null) {
        this.element.appendChild(this.pointer);
    }
};

/**
 * Remove info box. Called when removed from container.
 * @private
 */
HInfoBox.prototype.remove = function() {
    HOverlay.prototype.remove.call(this);

    HEvent.removeListeners(this.listeners);

    HEvent.trigger(this, HMap.EVENT_INFO_BOX_CLOSE, this);
};

/**
 * Set container pixel coordinates of info box.
 *
 * @param {HPoint} point Point representing pixel coordinates.
 * @private
 */
HInfoBox.prototype.setPoint = function(point) {
    this.point = new HPoint(point.x, point.y);
};

/**
 * Attach info box to marker. Will make the info box position be the same as
 * the attached marker's.
 *
 * @param {HMarker} marker Marker to attach to.
 */
HInfoBox.prototype.attachToMarker = function(marker) {
    this.marker = marker;  
};

/**
 * Get content of info box as DOM node.
 *
 * @returns {Element} Element containing content of info box.
 */
HInfoBox.prototype.getContentContainer = function() {
    return this.contentElement;
};

/**
 * Hide info box. This makes it invisible, it does not close it. Make
 * it visibile again by using show().
 */
HInfoBox.prototype.hide = function() {
    this.element.style.display = "none";
};

/**
 * Show info box.
 */
HInfoBox.prototype.show = function() {
    this.element.style.display = "block";
};

/**
 * Returns true if display property is set to hide info box.
 */
HInfoBox.prototype.isHidden = function() {
    return (this.element.style.display == "none");
};

/**
 * Update position of info box (depends on type of anchor point).
 * 
 * @private
 */
HInfoBox.prototype.update = function() {
    //console.log("update");
    var topOffset, bottomOffset;

    if (typeof(this.marker) != "undefined") {
        this.point = this.marker.point.clone()

        var icon = this.marker.getIcon();
        
        if (icon.infoBoxAnchorTop) {
            topOffset = icon.infoBoxAnchorTop.clone();
        }
        if (icon.infoBoxAnchorBottom) {
            bottomOffset = icon.infoBoxAnchorBottom.clone();
        }
    } else {
        topOffset = new HPoint(0, 0);
        bottomOffset = new HPoint(0, 0);        
    }
    
    var isIE = true; //HMapHelpers.isIE();
    
    switch (this.anchorType) {
        case HInfoBox.ANCHOR_TOPLEFT:
            this.point.x += bottomOffset.x - (HInfoBox.POINTER_WIDTH/2 + HInfoBox.POINTER_INDENT);
            this.point.y += HInfoBox.POINTER_HEIGHT + bottomOffset.y;
            break;
        case HInfoBox.ANCHOR_TOPRIGHT:
            if (!isIE) {
                this.point.x -= 10;
            }
            
            this.point.x += -(this.size.width - HInfoBox.POINTER_WIDTH/2 - HInfoBox.POINTER_INDENT) + bottomOffset.x;
            this.point.y += HInfoBox.POINTER_HEIGHT + bottomOffset.y;
            break;
        case HInfoBox.ANCHOR_BOTTOMLEFT:
            if (!isIE) {
                this.point.y -= 8;
            }
        
            this.point.x += topOffset.x - (HInfoBox.POINTER_WIDTH/2 + HInfoBox.POINTER_INDENT);
            this.point.y += -(this.size.height + HInfoBox.POINTER_HEIGHT) + topOffset.y;
            break;
        case HInfoBox.ANCHOR_BOTTOMRIGHT:
            if (!isIE) {
                this.point.y -= 8;
                this.point.x -= 10;
            }
            
            this.point.x += -(this.size.width - HInfoBox.POINTER_WIDTH/2 - HInfoBox.POINTER_INDENT) + topOffset.x;
            this.point.y += -(this.size.height + HInfoBox.POINTER_HEIGHT) + topOffset.y;
            break;
    }

    // position info box
    this.element.style.left = this.point.x + "px";
    this.element.style.top = this.point.y + "px";
};

/**
 * Pan info box.
 *
 * @param {Number} dx Delta X movement in pixels to pan.
 * @param {Number} dy Delta Y movement in pixels to pan.
 *
 * @private
 */
HInfoBox.prototype.pan = function(dx, dy) {
    this.point.x += dx;
    this.point.y += dy;
    
    this.element.style.left = this.point.x + "px";
    this.element.style.top = this.point.y + "px";
};

/**
 * Track mouse over state.
 * @private
 */
HInfoBox.prototype.mouseOver = function() {
    this.mouseIsOver = true;
};

/**
 * Track mouse over state.
 * @private
 */
HInfoBox.prototype.mouseOut = function() {
    this.mouseIsOver = false;
};

/**
 * Listens to map move event and updates anchor type and position of info box
 * depending on position in view.
 * 
 * @private
 */
HInfoBox.prototype.mapMove = function() {
    var anchorType = this.checkAnchorType();

    if (anchorType != this.anchorType) {
        this.setAnchorType(anchorType);
        this.update();
    }
};

/**
 * Check and return suitable anchor type based on position of info box.
 *
 * @return {Number} Anchor type.
 */
HInfoBox.prototype.checkAnchorType = function() {
    var markerAnchorOffsetY = 0;
    if (this.marker) {
        var icon = this.marker.getIcon();
        if (icon.infoBoxAnchorBottom && icon.infoBoxAnchorTop)  {
            markerAnchorOffsetY = Math.abs(icon.infoBoxAnchorBottom.y - icon.infoBoxAnchorTop.y);
        }
    }

    var G = this.hmap.globals;

    var anchorType = this.anchorType;

    if (this.point.y <= G.paddingTop &&
            (this.anchorType == HInfoBox.ANCHOR_BOTTOMLEFT ||
             this.anchorType == HInfoBox.ANCHOR_BOTTOMRIGHT)) {
        anchorType = anchorType - 2;
    } else if (this.point.y > (this.size.height + G.paddingTop + markerAnchorOffsetY + HInfoBox.FLIP_OFFSET_VERT) &&
            (this.anchorType == HInfoBox.ANCHOR_TOPLEFT ||
             this.anchorType == HInfoBox.ANCHOR_TOPRIGHT)) {
        anchorType = anchorType + 2;
    }

    var mapWidth = this.hmap.getSize().width;

    if (this.point.x <= (mapWidth - G.paddingRight - 2*this.size.width) &&
            (this.anchorType == HInfoBox.ANCHOR_TOPRIGHT ||
             this.anchorType == HInfoBox.ANCHOR_BOTTOMRIGHT)) {
        anchorType = anchorType - 1;
    } else if (this.point.x > (mapWidth - G.paddingRight - this.size.width - HInfoBox.FLIP_OFFSET_HORZ) &&
                   (this.anchorType == HInfoBox.ANCHOR_TOPLEFT ||
                    this.anchorType == HInfoBox.ANCHOR_BOTTOMLEFT)) {
        anchorType = anchorType + 1;
    }

    return anchorType;
};


/**
 * @fileOverview Polygon layer and vector graphics related functionality.
 */

/**
 * The PolyLayer class manages the vector based canvas layers. It is
 * automatically instantiated and initialized by HMap
 *
 * @class Polygon layer of map allowing vector based drawing.
 *
 * @param {HMap} hmap HMap instance.
 * 
 * @constructor
 */
function HPolyLayer(hmap) {
    this.hmap = hmap;

    this.globals = {
        isInitialized: false,
        mainCanvas: null,
        mainCtx: null,
        volatileCanvas: null,
        volatileCtx: null,
        width: 0,
        height: 0,
        polyContainer: null
    };

    this.polys = [];

    var G = this.globals;
    G.polyContainer = hmap.globals.polyContainer;
}

/**
 * Z-index for poly layer.
 * @private
 * @constant
 */
HPolyLayer.Z_INDEX = HOverlay.Z_INDEX_OFFSET - 1;


/**
 * Determines if clipping should be used to draw polyline. Currently this will
 * make it slower due to RT90<->pixel conversations.
 * @private
 * @constant
 */
HPolyLayer.CLIPPING = false;

/**
 * Initialize poly layer.
 * @private
 */
HPolyLayer.prototype.initialize = function() {
    var G = this.globals;

    HMapHelpers.clearElement(G.polyContainer);

    // detect if excanvas has been loaded
    if (!HMapHelpers.isCanvas()) {
        alert("ExCanvas not loaded!");
    }

    this.initCanvas(G.polyContainer, this.hmap.getSize());

    G.isInitialized = true;
};

/**
 * Returns true if poly layer is initialized.
 * 
 * @returns {Boolean} True if poly layer is initialized
 * @private
 */
HPolyLayer.prototype.isInitialized = function() {
    return this.globals.isInitialized;
};

/**
 * Init main and volatile canvas layers.
 *
 * @param {Element} container Append canvas to this element.
 * @param {HSize} size Size of canvas.
 * @private
 */
HPolyLayer.prototype.initCanvas = function(container, size) {
    var G = this.globals;

    G.size = size;

    HMapHelpers.applyStyle([container], {
        zIndex: HPolyLayer.Z_INDEX
    });

    // init main canvas
    G.mainCanvas = this.createCanvas(container, G.size.width, G.size.height);
    G.mainCanvas.id = "mainCanvas";
    G.mainCtx = G.mainCanvas.getContext("2d");

    // init volatile canvas
    G.volatileCanvas = this.createCanvas(container, G.size.width, G.size.height);
    G.volatileCanvas.id = "volatileCanvas";
    G.volatileCtx = G.volatileCanvas.getContext("2d");

    // set default styles
    this.setStyle(G.mainCtx, {
        strokeColor: "255, 5, 138",
        strokeOpacity: 0.8,
        fillColor: "255, 25, 13",
        fillOpacity: 0.6,
        weight: 4
    });

    this.setStyle(G.volatileCtx, {
        strokeColor: "165, 255, 0",
        strokeOpacity: 0.8,
        fillColor: "25, 255, 13",
        fillOpacity: 0.6,
        weight: 4
    });
};

/**
 * Create canvas with given size.
 *
 * @param {Element} container Container to append canvas element to.
 * @param {Number} width Width of canvas.
 * @param {Number} height Height of canvas.
 * @returns {Element} Canvas object.
 * @private
 */
HPolyLayer.prototype.createCanvas = function(container, width, height) {
    // init main canvas
    var canvas = document.createElement("canvas");
    container.appendChild(canvas);

    // emulated using excanvas on IE
    if(typeof G_vmlCanvasManager != 'undefined')
    {
        canvas = G_vmlCanvasManager.initElement(canvas);
    }

    canvas.width = width;
    canvas.height = height;

    canvas.setAttribute("width", width);
    canvas.setAttribute("height", height);

    HMapHelpers.applyStyle([canvas], {
        position: "absolute",
        overflow: "hidden",
        left: "0px",
        top: "0px",
        UserSelect: "none",
        KhtmlUserSelect: "none",
        MozUserSelect: "none",
        zIndex: HPolyLayer.Z_INDEX
    });

    return canvas;
};

/**
 * Add poly object to layer.
 * 
 * @param {HPoly} poly The poly object.
 * @param {Boolean} [skipUpdate] Set to true to skip triggering update. Makes
 *        adding many objects more efficient. Default false.
 */
HPolyLayer.prototype.addPoly = function(poly, skipUpdate) {
    var shouldNotUpdate = arguments[1] || false;

    poly.init(this.hmap);
    this.polys.push(poly);

    if (!shouldNotUpdate) {
        this.update(true);
    }
};

/**
 * Remove poly object from layer.
 */
HPolyLayer.prototype.removePoly = function(poly) {
    this.polys.remove(poly);
};

/**
 * Update all poly objects associated with layer.
 *
 * @param {Boolean} [skipClear] Set to true to skip clearing canvas context
 *        before updating poly objects. Default false.
 */
HPolyLayer.prototype.update = function(skipClear) {
    if (this.isInitialized()) {
        var shouldNotClear = arguments[1] || false;
        var bounds = this.getBounds();

        if (!shouldNotClear) {
            this.clearAll();
        }
        
        var len = this.polys.length;
        for (var i = 0; i < len; i++) {
            this.polys[i].update(bounds);
        }
    }
};

/**
 * Updates all poly objects when panning map.
 *
 * @param {Number} dx Delta distance in pixels.
 * @param {Number} dy Delta distance in pixels.
 */
HPolyLayer.prototype.pan = function(dx, dy) {
    // note: although this function just triggers a normal
    // update it is exposed and called to allow for possible
    // future optimizations without api changes.
    this.update();
};

/**
 * Get bounds of visible map view.
 * @private
 */
HPolyLayer.prototype.getBounds = function() {
    return this.hmap.getBounds();
};

/**
 * Get parent pane which contains canvas objects.
 *
 * @returns {Element} Polygon layer container.
 */
HPolyLayer.prototype.getPane = function() {
    return this.hmap.globals.polyContainer;
};

/**
 * Get volatile canvas context.
 *
 * @returns {CanvasContext2D} Volatile context.
 */
HPolyLayer.prototype.getCtxVolatile = function() {
    return this.globals.volatileCtx;
};

/**
 * Get main canvas context.
 *
 * @returns {CanvasContext2D} Main context.
 */
HPolyLayer.prototype.getCtxMain = function() {
    return this.globals.mainCtx;
};

/**
 * Clear canvas.
 *
 * @param {CanvasContext2D} ctx Context to clear.
 */
HPolyLayer.prototype.clear = function(ctx) {
    var G = this.globals;

    ctx.beginPath();
    ctx.clearRect(0, 0, G.size.width, G.size.height);
    ctx.closePath();
};

/**
 * Clear all canvases (main and volatile).
 */
HPolyLayer.prototype.clearAll = function() {
    this.clear(this.getCtxVolatile());
    this.clear(this.getCtxMain());
};

/**
 * Set default context style. If opacity is undefined 1 is used.
 *
 * @param {CanvasContext2D} ctx Context to apply style on.
 * @param {HPolyStyle} style Style to apply.
 */
HPolyLayer.prototype.setStyle = function(ctx, style) {
    if (style == null) return;
    
    if (style.strokeColor && style.strokeOpacity) {
        ctx.strokeStyle = "rgba(" + style.strokeColor + ", " + style.strokeOpacity + ")";
    } else if (style.strokeColor) {
        ctx.strokeStyle = "rgb(" + style.strokeColor + ")";
    }

    if (style.fillColor && style.fillOpacity) {
        ctx.fillStyle = "rgba(" + style.fillColor + ", " + style.fillOpacity + ")";
    } else if (style.fillColor) {
        ctx.fillStyle = "rgb(" + style.fillColor + ")";
    }

    // line width
    if (style.weight) {
        ctx.lineWidth = style.weight;
    }

    ctx.lineCap = "round";
    ctx.lineJoin = "round";
};

/**
 * Draw poly array using container pixel coordinates.
 *
 * @param {CanvasContext2D} ctx Canvas context to draw on.
 * @param {Array} poly Array of HPoint.
 * @param {Boolean} fill Set to true to fill polygon.
 */
HPolyLayer.prototype.drawPixelPoly = function(ctx, poly, fill) {
    if (poly.length == 0) return;

    if (HPolyLayer.CLIPPING) {
        // only draw points that results in visible results
        var size = this.hmap.getSize();
        var upperRight = new HPoint(size.width, 0);
        var lowerLeft = new HPoint(0, size.height);

        var skipCount = 0;

        ctx.beginPath();
        ctx.moveTo(poly[0].x, poly[0].y);
        var isMoved = true;
        var len = poly.length;
        for (var i = 1; i < len; i++) {
            if (this.intersectLineBounds(poly[i-1], poly[i], upperRight, lowerLeft)) {
                if (!isMoved) {
                    ctx.moveTo(poly[i-1].x, poly[i-1].y);
                }
                ctx.lineTo(poly[i].x, poly[i].y);
                isMoved = true;
            } else {
                skipCount++;
                isMoved = false;
            }
        }
    } else {
        // draw everything
        ctx.beginPath();
        ctx.moveTo(poly[0].x, poly[0].y);
        var len = poly.length;
        for (var i = 1; i < len; i++) {
            ctx.lineTo(poly[i].x, poly[i].y);
        }
    }

    fill ? ctx.fill() : ctx.stroke();
};

/**
 * Draw polyline.
 *
 * @param {CanvasContext2D} ctx Canvas context to draw on.
 * @param {HPolyline} line Polyline to draw.
 * @param {Boolean} [closePath] Optional parameter to override polyline's
 *                              close-path state.
 */
HPolyLayer.prototype.drawPolyline = function(ctx, line, closePath) {
    if (line.getVertexCount() == 0) return;
    var closePathState = arguments[2] || null;

    var poly = [];
    var len = line.getVertexCount();
    for (var i = 0; i < len; i++) {
        poly.push( this.hmap.fromRT90toContainerPixel(line.getVertex(i)) );
    }

    if (closePathState === null) {
        closePathState = line.isClosed();
    }

    ctx.save();
    this.setStyle(ctx, line.getStyle());
    this.drawPixelPoly(ctx, poly, closePathState);
    ctx.restore();
};

/**
 * Draw line between points.
 *
 * @param {CanvasContext2D} ctx Canvas context to draw on.
 * @param {HPointRT90} rt90point1 Start point.
 * @param {HPointRT90} rt90point2 End point.
 */
HPolyLayer.prototype.drawLine = function(ctx, rt90point1, rt90point2) {
    var point1 = this.hmap.fromRT90toContainerPixel(rt90point1);
    var point2 = this.hmap.fromRT90toContainerPixel(rt90point2);

    ctx.beginPath();
    ctx.moveTo(point1.x, point1.y);
    ctx.lineTo(point2.x, point2.y);
    ctx.stroke();
};

/**
 * Draw circle.
 *
 * @param {CanvasContext2D} ctx Canvas context to draw on.
 * @param {HPointRT90} rt90point Point where to draw center of circle.
 * @param {Number} radius Radius of circle.
 * @param {Boolean} filled Set to true if circle should be filled.
 */
HPolyLayer.prototype.drawCircle = function(ctx, rt90point, radius, filled) {
    var point = this.hmap.fromRT90toContainerPixel(rt90point);

    ctx.beginPath();
    ctx.arc(point.x, point.y, radius, 0, Math.PI*2, true);

    if (filled) {
        ctx.fill();
    } else {
        ctx.stroke();
    }
};

/**
 * Draw bounds rectangle.
 *
 * @param {CanvasContext2D} ctx Canvas context to draw on.
 * @param {HBoundsRT90} bounds Bounds rectangle.
 * @param {Boolean} filled Set to true if rectangle should be filled.
 */
HPolyLayer.prototype.drawBounds = function(ctx, bounds, filled) {
    var swPoint = this.hmap.fromRT90toContainerPixel(bounds.sw);
    var nePoint = this.hmap.fromRT90toContainerPixel(bounds.ne);

    ctx.beginPath();
    ctx.moveTo(swPoint.x, swPoint.y);
    ctx.lineTo(swPoint.x, nePoint.y);
    ctx.lineTo(nePoint.x, nePoint.y);
    ctx.lineTo(nePoint.x, swPoint.y);
    ctx.lineTo(swPoint.x, swPoint.y);

    if (filled) {
        ctx.fill();
    } else {
        ctx.stroke();
    }
};

/**
 * Checks if two lines intersect.
 *
 * @param {HPoint} line1start Start coordinate of first line.
 * @param {HPoint} line1end End coordinate of first line.
 * @param {HPoint} line2start Start coordinate of second line.
 * @param {HPoint} line2end End coordinate of second line.
 * @returns {Boolean} True if lines intersect.
 * @private
 */
HPolyLayer.prototype.intersectLineLine = function(line1start, line1end, line2start, line2end) {
	var denom = (line2end.y - line2start.y) * (line1end.x - line1start.x) - (line2end.x - line2start.x) * (line1end.y - line1start.y);
	var num1  = (line2end.x - line2start.x) * (line1start.y - line2start.y) - (line2end.y - line2start.y) * (line1start.x - line2start.x);
	var num2  = (line1end.x - line1start.x) * (line1start.y - line2start.y) - (line1end.y - line1start.y) * (line1start.x - line2start.x);

	if (denom == 0) {
		if (num1 == 0 && num2 == 0) {
			if (!(Math.min(line2start.x, line2end.x) > Math.max(line1start.x, line1end.x) ||
	 			  Math.max(line2start.x, line2end.x) < Math.min(line1start.x, line1end.x) ||
				  Math.min(line2start.y, line2end.y) > Math.max(line1start.y, line1end.y) ||
				  Math.max(line2start.y, line2end.y) < Math.min(line1start.y, line1end.y) )) {
				return true;
			}
		}
		return false;
	}

	var x = num1 / denom;
	var y = num2 / denom;

	return (!((x < 0 || x > 1) || (y < 0 || y > 1)));
};

/**
 * Checks if two lines specified in RT90 intersect.
 *
 * @param {HPointRT90} line1startRT90 Start coordinate of first line.
 * @param {HPointRT90} line1endRT90 End coordinate of first line.
 * @param {HPointRT90} line2startRT90 Start coordinate of second line.
 * @param {HPointRT90} line2endRT90 End coordinate of second line.
 * @returns {Boolean} True if lines intersect.
 */
HPolyLayer.prototype.intersectLineLineRT90 = function(line1startRT90, line1endRT90, line2startRT90, line2endRT90) {
    return this.intersectLineLine(
        this.hmap.fromRT90toContainerPixel(line1startRT90),
        this.hmap.fromRT90toContainerPixel(line1endRT90),
        this.hmap.fromRT90toContainerPixel(line2startRT90),
        this.hmap.fromRT90toContainerPixel(line2endRT90)
    );
};

/**
 * Checks if line intersect rectangular bound.
 *
 * @param {HPoint} linestart Start coordinate of first line.
 * @param {HPoint} lineend End coordinate of first line.
 * @param {HPoint} upperRight Upper right coordinate of rectangular bound.
 * @param {HPoint} lowerLeft Lower left coordinate of rectangular bound.
 * @returns {Boolean} True if line intersects rectangular bound.
 * @private
 */
HPolyLayer.prototype.intersectLineBounds = function(linestart, lineend, upperRight, lowerLeft) {
    if ( Math.min(linestart.x, lineend.x) >= Math.min(lowerLeft.x, upperRight.x) &&
		 Math.max(linestart.x, lineend.x) <= Math.max(lowerLeft.x, upperRight.x) &&
		 Math.min(linestart.y, lineend.y) >= Math.min(upperRight.y, lowerLeft.y) &&
		 Math.max(linestart.y, lineend.y) <= Math.max(upperRight.y, lowerLeft.y) ) {
		return true;
	}

	return (
        this.intersectLineLine(linestart, lineend, new HPoint(lowerLeft.x, upperRight.y), new HPoint(lowerLeft.x, lowerLeft.y)) ||      // left
		this.intersectLineLine(linestart, lineend, new HPoint(lowerLeft.x, lowerLeft.y), new HPoint(upperRight.x, lowerLeft.y)) ||		// bottom
        this.intersectLineLine(linestart, lineend, new HPoint(upperRight.x, lowerLeft.y), new HPoint(upperRight.x, upperRight.y)) ||	// right
		this.intersectLineLine(linestart, lineend, new HPoint(upperRight.x, upperRight.y), new HPoint(lowerLeft.x, upperRight.y))       // upper
    );
};

/**
 * Checks if line intersect rectangular bound specified in RT90 coordinates.
 *
 * @param {HPointRT90} linestartRT90 Start coordinate of first line.
 * @param {HPointRT90} lineendRT90 End coordinate of first line.
 * @param {HBoundsRT90} bounds Rectangular bound to check.
 * @returns {Boolean} True if line intersects rectangular bound.
 */
HPolyLayer.prototype.intersectLineBoundsRT90 = function(linestartRT90, lineendRT90, bounds) {
    return this.intersectLineBounds(
        this.hmap.fromRT90toContainerPixel(linestartRT90),
        this.hmap.fromRT90toContainerPixel(lineendRT90),
        this.hmap.fromRT90toContainerPixel(bounds.ne),
        this.hmap.fromRT90toContainerPixel(bounds.sw)
    );
};

/**
 * Object literal representing style used by poly objects.
 *
 * @class Object literal representing style used by poly objects.
 *
 * @property {String} fillColor Fill color.
 * @property {Number} fillOpacity Opacity for fill color as a value between 0 and 1.
 * @property {String} strokeColor Stroke color.
 * @property {Number} strokeOpacity Opacity for strokes as a value between 0 and 1.
 * @property {Number} weight Line width of stroke.
 */
function HPolyStyle() {
    this.fillColor = "192, 192, 16";
    this.fillOpacity = 0.8;
    this.strokeColor = "192, 16, 192";
    this.strokeOpacity = 0.8;
    this.weight = 2;
}

/**
 * Poly object constructor.
 * 
 * @class Abstract class for poly objects used in poly layer.
 * @constructor
 */
function HPoly() {
    this.style = null;
}

/**
 * Initiate.
 *
 * @param {HMap} hmap The HMap instance.
 */
HPoly.prototype.init = function(hmap) {
    this.hmap = hmap;
    this.polyLayer = hmap.globals.polyLayer;
};

/**
 * Called when should be removed from map.
 */
HPoly.prototype.remove = function() {
    var G = this.polyLayer.globals;

    this.polyLayer.clear(G.mainCtx);
};

/**
 * Is called when poly object needs to be updated and redrawn. Bounds of
 * currently visibile area on the map is given so the object may determine if
 * it needs to draw itself or not.
 *
 * @param {HBoundsRT90} bounds
 */
HPoly.prototype.update = function(bounds) {
    // should be implemented by object inheriting HPoly
};

/**
 * Set style for poly object.
 * 
 * @param {HPolyStyle} style Style.
 */
HPoly.prototype.setStyle = function(style) {
    this.style = style;
};

/**
 * Get current style of poly object (or null if not set).
 * 
 * @returns {HPolyStyle}
 */
HPoly.prototype.getStyle = function() {
    return this.style;
};

/**
 * HPolyline constructor.
 * 
 * @class The HPolyline draws a series of lines (or a filled polygon) on the map.
 *
 * @augments HPoly
 * @constructor
 */
function HPolyline() {
    this.pathClosed = false;  // should be drawn as polygon (closed path) or not

    this.vertexList = [];
}

// HPolyline inherits from HPoly.
HPolyline.prototype = new HPoly();

/**
 * Update and redraw.
 */
HPolyline.prototype.update = function() {
    var G = this.polyLayer.globals;

    this.polyLayer.drawPolyline(G.mainCtx, this);
};

/**
 * Insert vertex into polyline at given index.
 *
 * @param {Number} index Index of polyline.
 * @param {HPointRT90} rt90point RT90 coordinate for vertex.
 */
HPolyline.prototype.insertVertex = function(index, rt90point) {
    this.vertexList.splice(index, 0, rt90point);
};

/**
 * Add vertex to end of polyline.
 * 
 * @param {HPointRT90} rt90point RT90 coordinate for vertex.
 */
HPolyline.prototype.addVertex = function(rt90point) {
    this.vertexList.push(rt90point);
};

/**
 * Delete vertex at given index.
 * 
 * @param {Number} index Index of polyline.
 */
HPolyline.prototype.deleteVertex = function(index) {
    this.vertexList.splice(index, 1);
};

/**
 * Get vertex at given index of polyline.
 *
 * @param {Number} index Index of polyline.
 * @returns {HPointRT90} RT90 vertex at given index.
 */
HPolyline.prototype.getVertex = function(index) {
    return this.vertexList[index];
};

/**
 * Get number of verticies in polyline.
 *
 * @returns {Number} Vertex count.
 */
HPolyline.prototype.getVertexCount = function() {
    return this.vertexList.length;
};

/**
 * Reset polyline. After reset the polyline will not contain any vertices.
 */
HPolyline.prototype.reset = function() {
    this.vertexList = [];
};

/**
 * Get bounds of polyline.
 * 
 * @returns {HBoundsRT90} Bounds of polyline.
 */
HPolyline.prototype.getBounds = function() {
    if (this.vertexList.length == 0) {
        return null;
    }

    var bounds = new HBoundsRT90(this.vertexList[0], this.vertexList[0]);

    var len = this.vertexList.length;
    for (var i = 1; i < len; i++) {
        bounds.extend(this.vertexList[i]);
    }

    return bounds;
};

/**
 * Get length of polyline in meters.
 *
 * @returns {Number} Length in meters.
 */
HPolyline.prototype.getLength = function() {
    var dist = 0;
    var len = this.vertexList.length;
    for (var i = 1; i < len; i++) {
        dist += this.vertexList[i - 1].distanceTo(this.vertexList[i]);
    }

    return dist;
};

/**
 * Get the area in square meters of the polygon made up of the current
 * polyline.
 *
 * @returns {Number} Polygon area in square meters.
 */
HPolyline.prototype.getArea = function() {
    // no area if less than three vertices
    if (this.getVertexCount() < 3) {
        return 0;
    }

    var len = this.vertexList.length;

    var area = 0;

    var prev = this.vertexList[len - 1];
    var curr = null;

    
    for (var i = 0; i < len; i++) {
        curr = this.vertexList[i];
        area += (prev.east * curr.north) - (prev.north * curr.east);
        prev = curr;
    }

    return Math.abs(area / 2);
};

/**
 * Closed state determines if polyline should be drawn as a line or as a
 * closed filled polygon.
 *
 * @param {Boolean} closed Closed state.
 */
HPolyline.prototype.setClosed = function(closed) {
    this.pathClosed = closed;
};

/**
 * Get closed status.
 *
 * @returns {Boolean} Current close state of polyline.
 */
HPolyline.prototype.isClosed = function() {
    return this.pathClosed;
};

/**
 * Extended polyline with vertex optimization based on current map resolution. It
 * will automatically discard vertices closer to each other than a threshold value.
 *
 * Note that HPolylineExtended is immutable once added to the poly layer. That means
 * that adding and removing vertices will not affect the polyline rendered.
 *
 * @param {Number} [threshold] Minimum distance between vertices in pixels.
 *
 * @constructor
 */
function HPolylineExtended(threshold) {
    this.pixelThreshold = arguments[0] || HPolylineExtended.THRESHOLD_PIXELS;

    this.reset();
}

HPolylineExtended.prototype = new HPolyline();

/**
 * Default threshold value in pixels.
 * @private
 * @const
 */
HPolylineExtended.THRESHOLD_PIXELS = 7;

/**
 * Update and redraw an optimized polyline for the current map
 * resolution will be drawn.
 * @private
 */
HPolylineExtended.prototype.update = function() {
    var G = this.polyLayer.globals;

    // get polyline based on current map resolution
    var polyline = this.getPolyline(this.hmap.getResolution());

    // update it with current style
    polyline.setStyle(this.getStyle());

    this.polyLayer.drawPolyline(G.mainCtx, polyline);
};

/**
 * Get optimized polyline for given resolution.
 *
 * @param {Number} resolution Current resolution.
 * @returns {HPolyline} Optimized polyline.
 * @private
 */
HPolylineExtended.prototype.getPolyline = function(resolution) {
    if (!this.polylineCache[resolution]) {
        this.polylineCache[resolution] = this.createOptimized(resolution);
    }

    return this.polylineCache[resolution];
};

/**
 * Resets vertex list and any precalculated optimized polylines.
 */
HPolylineExtended.prototype.reset = function() {
    this.vertexList = [];
    this.polylineCache = {};
};

/**
 * Creates an optimized version of the curreny polyline instance optimized
 * for the given resolution.
 *
 * @param {Number} resolution Resolution to optimize for.
 * @returns {HPolyline} Optimized polyline object.
 * @private
 */
HPolylineExtended.prototype.createOptimized = function(resolution) {
    var threshold = Math.pow(this.pixelThreshold * resolution, 2);

    var line = new HPolyline();
    var count = this.getVertexCount();
    
    // return immediately if polyline empty
    if (count == 0) {
        return line;
    }

    // always include first vertex
    line.addVertex(this.getVertex(0));

    var prev = line.getVertex(0);
    var current = null;

    for (var i = 1; i < count - 1; i++) {
        current = this.getVertex(i);

        // add point if distance larger than threshold
        if (Math.pow(prev.east - current.east, 2) + Math.pow(prev.north - current.north, 2) > threshold) {
            line.addVertex(current);
            prev = current;
        }
    }

    // always include last vertex
    if (count > 1) {
        line.addVertex(this.getVertex(count - 1));
    }

    return line;
}

/**
 * Poly dot (uses current main context style).
 * 
 * @param {HPointRT90} rt90point Position of dot.
 * @param {Number} radius Radius of dot.
 * @param {Boolean} filled Set to true if dot should be filled.
 * @constructor
 */
function HPolyDot(rt90point, radius, filled) {
    this.rt90point = rt90point;
    this.radius = radius;
    this.filled = filled;
}

// HPolyDot inherits from HPoly.
HPolyDot.prototype = new HPoly();

/**
 * Redraw dot. Uses context style for drawing.
 * 
 * @param {HBoundsRT90} bounds Visible bound of map.
 */
HPolyDot.prototype.update = function(bounds) {
    if (bounds.containsRT90(this.rt90point)) {
        this.polyLayer.drawCircle(this.polyLayer.getCtxMain(), this.rt90point, this.radius, this.filled);
    }
};

/**
 * @fileOverview Augmented JavaScript functions.
 */

/**
 * Array augmentations.
 *
 * @name Array
 * @class Array object augments.
 */

if (!Array.prototype.indexOf) {
    /**
     * Array.indexOf() functionality (exists in >FF 1.5 but not in IE)
     *
     * @param {Object} elem Element to find.
     * @param {Number} [fromIndex] Position in array to look from.
     * @returns {Number} Returns -1 if not found, otherwise index where element
     *                   found.
     */    
    Array.prototype.indexOf = function(elem, fromIndex) {
        var len = this.length;
        
        var from = Number(arguments[1]) || 0;
        from = (from < 0) ? Math.ceil(from) : Math.floor(from);
        if (from < 0) {
            from += len;
        }
        
        for (; from < len; from++) {
            if (from in this && this[from] === elem) {
                return from;
            }
        }
        
        return -1;
    };
}

if (!Array.prototype.remove) {
    /**
     * Array.remove() convenience method to remove element from array.
     * 
     * @param {Object} elem Element to remove.
     */
    Array.prototype.remove = function(elem) {
        var index = this.indexOf(elem);
        
        if (index !== -1) {
            this.splice(index, 1);
        }        
    };
}

if (!Array.prototype.insert) {
    /**
     * Array.insert() convenience method to insert element into array.
     *
     * @param {Number} index Index in array where to insert element.
     * @param {Object} elem Element to insert.
     */
    Array.prototype.insert = function(index, elem) {
        this.splice(index, 0, elem);
    };
}

/**
 * Functions related to coordinate transformation in longitude/latitude.
 *
 * @name Math
 * @class Math object augments.
 */

if (!Math.sinh) {
    /**
     * Hyperbolic sine
     */
    Math.sinh = function(x) {
        return (Math.exp(x)-Math.exp(-x))/2;
    };
}

if (!Math.cosh) {
    /**
     * Hyperbolic cosine
     */
    Math.cosh = function(x) {
        return (Math.exp(x)+Math.exp(-x))/2;
    };
}

if (!Math.atanh) {
    /**
     * Hyperbolic arc tangent
     */
    Math.atanh = function(x) {
        return (1/2)*(Math.log(x+1) - Math.log(1-x));
    };
}

if (!Math.toDegrees) {
    /**
     * Radians to degrees.
     *
     * @param {Number} rad Radians.
     * @returns {Number} Degrees.
     */
    Math.toDegrees = function(rad) {
        return rad*360/(2*Math.PI);
    };
}

if (!Math.toRadians) {
    /**
     * Degrees to radians.
     *
     * @param {Number} deg Degrees.
     * @returns {Number} Radians.
     */
    Math.toRadians = function(deg) {
        return (deg*2*Math.PI)/360;
    };
}

if (!Math.decimalToHex) {
    /**
     * Decimal to hexadecimal conversion.
     *
     * @param {Number} dec Number.
     * @returns {String} Hex.
     */
    Math.decimalToHex = function(dec) {
        return dec.toString(16);
    }
}

if (!Math.hexToDecimal) {
    /**
     * Hexadecimal to decimal conversion.
     *
     * @param {String} hex Hexadecimal.
     * @returns {Number} Decimal.
     */
    Math.hexToDecimal = function(hex) {
        return parseInt(hex, 16);
    }
}
/**
 * @fileOverview Map provider implementations.
 */

/**
 * @class HMapProvider abstract class, must be implemented by a map provider.
 *
 * @constructor
 */
function HMapProvider() {

}

/**
 * Initiates DOM elements representing tiles and adds them to
 * parent container. Typically uses HMap.getPaneMap() and
 * HMap.getTileCount(). Should set this.container to the DOM
 * element the tiles are added to (used by remove()).
 *
 * @param {HMap} hmap HMap instance.
 */
HMapProvider.prototype.initTiles = function(hmap) {
    this.container = this.hmap.getPaneMap();
};

/**
 * Called when map provider is removed from map. Should clean up and remove
 * any elements added to parent in initTiles().
 */
HMapProvider.prototype.remove = function() {
    for (var i = 0; i < this.elements.length; i++) {
        this.container.removeChild(this.elements[i]);
    }
};

/**
 * Update tile content.
 *
 * @param {Number} tileIndex Index of tile to update.
 * @param {Number} resolution Current map resolution.
 * @param {Number} mapRow Current map row of tile.
 * @param {Number} mapColumn Current map column of tile.
 */
HMapProvider.prototype.updateTile = function(tileIndex, resolution, mapRow, mapColumn) {

};

/**
 * Update tile pixel position.
 *
 * @param {Number} tileIndex Index of tile to update.
 * @param {HPoint} point Point representing pixel position of tile.
 */
HMapProvider.prototype.updatePosition = function(tileIndex, point) {
    this.elements[tileIndex].style.left = point.x + "px";
    this.elements[tileIndex].style.top = point.y + "px";
};

/**
 * Set current map type.
 *
 * @param {Number} mapType Set current map type.
 */
HMapProvider.prototype.setMapType = function(mapType) {

};

/**
 * Get current map type.
 *
 * @returns {Number} Current map type.
 */
HMapProvider.prototype.getMapType = function() {
    return 0;
};

/**
 * Get available map types.
 *
 * @returns {array} An array of available map types.
 */
HMapProvider.prototype.getMapTypes = function() {
    return [0];
};

/**
 * Get available minimum zoom level. If no map type is given the current
 * one is assumed.
 *
 * @param {Number} [mapType] Map type.
 * @returns {Number} Zoom level.
 */
HMapProvider.prototype.getMinZoomLevel = function(mapType) {
    return 1;
};

/**
 * Get available maximum zoom level. If no map type is given the current
 * one is assumed.
 *
 * @param {Number} [mapType] Map type.
 * @returns {Number} Zoom level.
 */
HMapProvider.prototype.getMaxZoomLevel = function(mapType) {
    return 1;
};

/**
 * This function is called whenever the map has changed so the map provider may
 * be able to update its status (available zoom levels).
 */
HMapProvider.prototype.initStatusUpdate = function() {

};

/**
 * Standard map provider constructor.
 * 
 * @class Hitta.se standard map provider with normal map and satellite view.
 *
 * @augments HMapProvider
 * @constructor
 * @private
 */
function HStandardMapProvider(hmap) {
    this.hmap = hmap;

    // default map type
    this.mapType = HStandardMapProvider.MAP_TYPE_NORMAL;

    // set default zoom levels (updated through jsonp callback)    
    HStandardMapProvider.zoomLevel = [];
    var mapTypeCount = this.getMapTypes().length;
    for (var i = 0; i < mapTypeCount; i++) {
        HStandardMapProvider.zoomLevel[i] = {min: 1, max: 9};
    }

    this.overrideMinZoomLevel = 0;
    this.overrideMaxZoomLevel = 10;

    this.elements = [];
}

// HStandardMapProvider inherits from HMapProvider.
HStandardMapProvider.prototype = new HMapProvider();

/**
 * Base of URL used to retrieve tiles.
 * Base of URL used to retrieve tiles.
 * @constant
 */
HStandardMapProvider.TILE_BASE_URL=".hitta.se/static/tile/";

/**
 * URL of image used for not found tiles.
 * @constant
 */
HStandardMapProvider.TILE_MAP_NOT_FOUND_URL=".hitta.se/static/data/tiles/empty/blue-map.gif";
HStandardMapProvider.TILE_SAT_NOT_FOUND_URL=".hitta.se/static/data/tiles/empty/blue-sat.jpg";
/**
 * Subdomains available for domain sharding of tiles.
 * @constant
 */
HStandardMapProvider.TILE_DOMAINS=["static"];

/**
 * Number of subdomains available for domain sharding of tiles.
 * @constant
 */
HStandardMapProvider.TILE_DOMAINS_COUNT = HStandardMapProvider.TILE_DOMAINS.length;

/**
 * URL of tile used when loading.
 * @constant
 */
HStandardMapProvider.TILE_LOADING_URL = "http://www.codetouch.com/hitta/loadingtile.gif";

/**
 * URL of JSONP service used for status updates.
 * @constant
 */
HStandardMapProvider.TILE_STATUS_UPDATE_BASE_URL = "http://static.hitta.se/static/zoomlevel/";

/**
 * Map type constant for normal map.
 * @constant
 */
HStandardMapProvider.MAP_TYPE_NORMAL = 0;

/**
 * Map type constant for satellite map.
 * @constant
 */
HStandardMapProvider.MAP_TYPE_SATELLITE = 1;

/**
 * Timeout in milliseconds for status update JSONP callback.
 * @constant
 */
HStandardMapProvider.STATUS_UPDATE_TIMEOUT = 2000;

/**
 * Initiates DOM elements representing tiles and adds them to
 * parent container.
 *
 * @param {HMap} hmap HMap instance.
 */
HStandardMapProvider.prototype.initTiles = function(hmap) {
    this.hmap = hmap;
    var parent = this.hmap.getPaneMap();
    var tileCount = this.hmap.getTileCount();

    this.elements.length = tileCount;
    
    for (var i = 0; i < tileCount; i++) {
        var img = document.createElement("img");

        HMapHelpers.applyStyle([img], {
            position: "absolute",
            top: -HTile.TILE_WIDTH + "px",
            left: -HTile.TILE_HEIGHT + "px",
            width: HTile.TILE_WIDTH + "px",
            height: HTile.TILE_HEIGHT + "px"
        });
        //img.src = HStandardMapProvider.TILE_LOADING_URL;

        this.elements[i] = img;

        parent.appendChild(img);
    }

    this.container = parent;
};

/**
 * Update tile content.
 * 
 * @param {Number} tileIndex Index of tile to update.
 * @param {Number} resolution Current map resolution.
 * @param {Number} mapRow Current map row of tile.
 * @param {Number} mapColumn Current map column of tile.
 */
HStandardMapProvider.prototype.updateTile = function(tileIndex, resolution, mapRow, mapColumn) {
    var url = "";
    if (mapRow < 0 || mapColumn < 0) {
        url= "http://static" + ((this.mapType==HStandardMapProvider.MAP_TYPE_SATELLITE) ? HStandardMapProvider.TILE_SAT_NOT_FOUND_URL : HStandardMapProvider.TILE_MAP_NOT_FOUND_URL);
    } else {
        //var shard = (Math.abs(mapColumn + mapRow)) % HStandardMapProvider.TILE_DOMAINS_COUNT;
        // "http://" + HStandardMapProvider.TILE_DOMAINS[shard] + HStandardMapProvider.TILE_BASE_URL + this.mapType + "/" + resolution + "/" + mapRow + "/" + mapColumn;
        url = "http://static" + HStandardMapProvider.TILE_BASE_URL + this.mapType + "/" + resolution + "/" + mapRow + "/" + mapColumn;
    }

    this.elements[tileIndex].src = url;
};

/**
 * Set current map type.
 *
 * @param {Number} mapType Set current map type.
 */
HStandardMapProvider.prototype.setMapType = function(mapType) {
    this.mapType = mapType;
};

/**
 * Get current map type.
 *
 * @returns {Number} Current map type.
 */
HStandardMapProvider.prototype.getMapType = function() {
    return this.mapType;
};

/**
 * Get available map types.
 *
 * @returns {array} An array of available map types.
 */
HStandardMapProvider.prototype.getMapTypes = function() {
    return [HStandardMapProvider.MAP_TYPE_NORMAL, HStandardMapProvider.MAP_TYPE_SATELLITE];
};

/**
 * Get available minimum zoom level. If no map type is given the current
 * one is assumed.
 *
 * @param {Number} [mapType] Map type.
 * @returns {Number} Zoom level.
 */
HStandardMapProvider.prototype.getMinZoomLevel = function(mapType) {
    var type = arguments[0] || this.mapType;

    return Math.max(HStandardMapProvider.zoomLevel[type].min, this.overrideMinZoomLevel);
};

/**
 * Get available maximum zoom level. If no map type is given the current
 * one is assumed.
 *
 * @param {Number} [mapType] Map type.
 * @returns {Number} Zoom level.
 */
HStandardMapProvider.prototype.getMaxZoomLevel = function(mapType) {
    var type = arguments[0] || this.mapType;
    
    return Math.min(HStandardMapProvider.zoomLevel[type].max, this.overrideMaxZoomLevel);
};

/**
 * Set explicit minimum zoom level.
 *
 * @param {Number} zoomLevel Zoom level to set as minimum.
 */
HStandardMapProvider.prototype.setMinZoomLevel = function(zoomLevel) {
    this.overrideMinZoomLevel = zoomLevel;
};

/**
 * Set explicit maximum zoom level.
 *
 * @param {Number} zoomLevel Zoom level to set as max.
 */
HStandardMapProvider.prototype.setMaxZoomLevel = function(zoomLevel) {
    this.overrideMaxZoomLevel = zoomLevel;
};

/**
 * Callback function used for JSONP call to update status (zoom level, ...).
 * 
 * @param {Object} obj JSONP object.
 * @private
 */
HStandardMapProvider.prototype.updateStatusCallback = function(obj) {
    // static variable used since context is lost upon callback
    HStandardMapProvider.zoomLevel[0].max = obj.v0;
    HStandardMapProvider.zoomLevel[1].max = obj.v1;

    HEvent.trigger(HStandardMapProvider, HMap.EVENT_INTERNAL_MAP_STATUS_UPDATE, HStandardMapProvider.zoomLevel);
    HEvent.trigger(HStandardMapProvider, HMap.EVENT_MAP_STATUS_UPDATE, HStandardMapProvider.zoomLevel);
};

/**
 * Initiate status update using JSONP call.
 */
HStandardMapProvider.prototype.initStatusUpdate = function() {
    HStandardMapProvider.UPDATE = this.updateStatusCallback;
    
    var center = this.hmap.getCenter();

    // temp fix to avoid exception on zoom level service
    if (!(isNaN(parseInt(center.north)) || isNaN(parseInt(center.east)))) {
        var url = HStandardMapProvider.TILE_STATUS_UPDATE_BASE_URL + parseInt(center.north) + "/" + parseInt(center.east) + "?callback=HStandardMapProvider.UPDATE";
        HMapHelpers.initJSONP(url, HStandardMapProvider.STATUS_UPDATE_TIMEOUT);
    }
};
/**
 * @fileOverview Longitude/latitude transformation.
 */

/**
 * @class HPointLatLng represents a geographical coordinate described using latitude
 * and longitude.
 * 
 * HMap is based around the RT90 coordinate system, therefore most of the
 * functionality is provided in the {@link HPointRT90} object.
 *
 * @param {Number} lat Latitude in decimal notation.
 * @param {Number} lng Longitude in decimal notation.
 * @constructor
 *
 * @see HPointRT90
 */
var HPointLatLng = function(lat, lng) {
	this.lat = parseFloat(lat);
	this.lng = parseFloat(lng);
};

/**
 * Returns true if given point is equal to current.
 *
 * @param {HPointLatLng} point Point to compare with.
 */
HPointLatLng.prototype.equals = function(point) {
    return (this.lat == point.lat && this.lng == point.lng);
};

/**
 * Returns latitude in decimal notation.
 *
 * @returns {Number} Latitude in decimal notation.
 */
HPointLatLng.prototype.getLat = function() {
    return this.lat;
};

/**
 * Returns longitude in decimal notation.
 *
 * @returns {Number} Longitude in decimal notation.
 */
HPointLatLng.prototype.getLng = function() {
    return this.lng;
};

/**
 * Returns string describing longitude and latitude.
 *
 * @returns {String}
 */
HPointLatLng.prototype.toString = function() {
	var lat = HPointLatLng.fromDecimalToDegree(this.lat);
	var lng = HPointLatLng.fromDecimalToDegree(this.lng);

    return "Lat N " + HPointLatLng.fromDegreeToString(lat.deg, lat.min, lat.sec) + "\n" +
           "Lon E " + HPointLatLng.fromDegreeToString(lng.deg, lng.min, lng.sec);
};

/**
 * Returns string description of axis.
 *
 * @param {Number} deg Degrees.
 * @param {Number} min Minutes.
 * @param {Number} sec Seconds.
 * @returns {String} String description of axis.
 * @static
 */
HPointLatLng.fromDegreeToString = function(deg, min, sec) {
    return deg + "&#176; " + min + "&#8242; " + sec + "&#8243;";
};

/**
 * Convert axis in decimal to degree notation.
 *
 * @param {Number} dec Axis in decimal.
 * @returns {Object} Object with properties deg, min and sec.
 * @static
 */
HPointLatLng.fromDecimalToDegree = function(dec) {
	var deg = parseInt(dec);
	var min = parseInt((dec-deg)*60);
	var sec = parseInt(((dec-deg)*60-min)*60);
	return {deg:deg, min:min, sec:sec};
};

/**
 * Convert axis in degrees to decimal notation.
 *
 * @param {Number} deg Degrees.
 * @param {Number} min Minutes.
 * @param {Number} sec Seconds.
 * @returns {Number} Axis in decimal.
 * @static
 */
HPointLatLng.fromDegreeToDecimal = function(deg, min, sec) {
	return parseFloat(deg + (min/60) + (sec/3600));
};

/**
 * Ellipsoid constructor.
 *
 * @class Internal class used for ellipsoid calculations when doing longitude/latitude transformations.
 *
 * @param {Number} a Semi-major axis.
 * @param {Number} f Flattening.
 * @constructor
 * @private
 */
var HTransformEllipsoid = function(a, f) {
	a = this.a = parseFloat(a);
	f = this.f = parseFloat(f);

	var e2 = this.e2 = f*(2-f);
	var e4 = Math.pow(e2,2);
	var e6 = Math.pow(e2,3);
	var e8 = Math.pow(e2,4);

	var n = this.n = f/(2-f);
	var n2 = Math.pow(n,2);
	var n3 = Math.pow(n,3);
	var n4 = Math.pow(n,4);

	this.ah = (a/(1+n))*(1+(1/4)*n2+(1/64)*n4);

	// d (delta) are used in (x,y)->(lat,lng)
	var d = this.d = [];
	d[0] = (1/2)*n - (2/3)*n2 + (37/96)*n3 - (1/360)*n4;
	d[1] = (1/48)*n2 + (1/15)*n3 - (437/1440)*n4;
	d[2] = (17/480)*n3 - (37/840)*n4;
	d[3] = (4397/161280)*n4;

	// abc coefficients are used in (x,y)->(lat,lng)
	var abc = this.abc = [];
	abc[0] = e2 + e4 + e6 + e8;
	abc[1] = -(1/6)*(7*e4 + 17*e6 + 30*e8);
	abc[2] = (1/120)*(224*e6 + 889*e8);
	abc[3] = -(1/1260)*(4279*e8);

	// b (beta) are used in (lat,lng)->(lat.lng)
	var b = this.b = [];
	b[0] = (1/2)*n - (2/3)*n2 + (5/16)*n3 + (41/180)*n4;
	b[1] = (13/48)*n2 - (3/5)*n3 + (557/1440)*n4;
	b[2] = (61/240)*n3 - (103/140)*n4;
	b[3] = (49561/161280)*n4;

	// xyz coefficients are used in (lat,lng)->(x,y)
	var xyz = this.xyz = [];
	xyz[0] = e2;
	xyz[1] = (1/6)*(5*e4-e6);
	xyz[2] = (1/120)*(104*e6-45*e8);
	xyz[3] = (1/1260)*(1237*e8);
};

/**
 * Transform calculations for longitude/latitude. This class is not intended to
 * be called directly, instead the fromLatLngToRT90 and fromRT90ToLatLng
 * functions in HMap should be used.
 *
 * @class Internal class for transformation calcuations from and to longitude
 * and latitude coordinates.
 *
 * @constructor
 * @private
 */
var HTransform = function() {
	this.e = new HTransformEllipsoid(this.SEMI_MAJOR_AXIS,this.FLATTENING);
};

HTransform.prototype = {
	SEMI_MAJOR_AXIS: 6378137,
	FLATTENING: 1/298.257222101,
	FALSE_NORTHING: -667.711,
	FALSE_EASTING: 1500064.274,
	SCALE_REDUCTION: 1.00000561024,
	CENTRAL_MERIDIAN: Math.toRadians(HPointLatLng.fromDegreeToDecimal(15, 48, 22.624306))
};

/**
 * Converts geographical coordinate from RT90 to latitude/longitude.
 *
 * @param {HPointRT90} rt90point RT90 point.
 * @returns {HPointLatLng} Coordinate as latitude/longitude.
 *
 * @see HMap#fromRT90ToLatLng
 */
HTransform.prototype.fromRT90ToLatLng = function(rt90point) {
	var xi = (rt90point.north-this.FALSE_NORTHING)/(this.SCALE_REDUCTION*this.e.ah);
	var eta = (rt90point.east-this.FALSE_EASTING)/(this.SCALE_REDUCTION*this.e.ah);

	var xip = xi;
	var etap = eta;
	var d = this.e.d;
	for (var i=0;i<d.length;i++) {
		xip -= d[i]*Math.sin((2*i+2)*xi)*Math.cosh((2*i+2)*eta);
		etap -= d[i]*Math.cos((2*i+2)*xi)*Math.sinh((2*i+2)*eta);
	}

	var cLat = Math.asin(Math.sin(xip)/Math.cosh(etap));
	var dLng = Math.atan(Math.sinh(etap)/Math.cos(xip));

	var sinCLat = Math.sin(cLat);
	var cosCLat = Math.cos(cLat);
	var abc = this.e.abc;

	var lat = Math.toDegrees(cLat + sinCLat*cosCLat*(abc[0]+abc[1]*Math.pow(sinCLat,2)+abc[2]*Math.pow(sinCLat,4)+abc[3]*Math.pow(sinCLat,6)));
	var lng = Math.toDegrees(this.CENTRAL_MERIDIAN + dLng);

	return new HPointLatLng(lat, lng);
};

/**
 * Converts geographical coordinate from latitude/longitude to RT90.
 *
 * @param {HPointLatLng} pointLatLng Coordinate in latitude/longitude.
 * @returns {HPointRT90} Coordinate in RT90.
 *
 * @see HMap#fromLatLngToRT90
 */
HTransform.prototype.fromLatLngToRT90 = function(pointLatLng) {
	var lat = Math.toRadians(pointLatLng.getLat());
	var lng = Math.toRadians(pointLatLng.getLng());

	var sinLat = Math.sin(lat);
	var cosLat = Math.cos(lat);
	var xyz = this.e.xyz;

	var cLat = lat - sinLat*cosLat*(xyz[0] + xyz[1]*Math.pow(sinLat,2) + xyz[2]*Math.pow(sinLat,4) + xyz[3]*Math.pow(sinLat,6));
	var dLng = lng - this.CENTRAL_MERIDIAN;

	var xip = Math.atan(Math.tan(cLat)/Math.cos(dLng));
	var etap = Math.atanh(Math.cos(cLat)*Math.sin(dLng));
	var b = this.e.b;

	var x = this.SCALE_REDUCTION*this.e.ah*(xip +
		b[0]*Math.sin(2*xip)*Math.cosh(2*etap) +
		b[1]*Math.sin(4*xip)*Math.cosh(4*etap) +
		b[2]*Math.sin(6*xip)*Math.cosh(6*etap) +
		b[3]*Math.sin(8*xip)*Math.cosh(8*etap)) + this.FALSE_NORTHING;

	var y = this.SCALE_REDUCTION*this.e.ah*(etap +
		b[0]*Math.cos(2*xip)*Math.sinh(2*etap) +
		b[1]*Math.cos(4*xip)*Math.sinh(4*etap) +
		b[2]*Math.cos(6*xip)*Math.sinh(6*etap) +
		b[3]*Math.cos(8*xip)*Math.sinh(8*etap)) + this.FALSE_EASTING;

	return new HPointRT90(Math.round(x), Math.round(y));
};

/**
 * @class A HGeoPoint represents a geographical point independent of coordinate
 * system (used by HGeoTransform).
 *
 * @param {Number} north Northern coordinate.
 * @param {Number} east Eastern coordinate.
 *
 * @property {Number} north Northern coordinate.
 * @property {Number} east Eastern coordinate.
 *
 * @constructor
 * @see HGeoTransform
 */
function HGeoPoint(north, east) {
    this.north = north;
    this.east = east;
}

/**
 * @class Contains static methods for geo transformations between different coordinate
 * systems.
 */
var HGeoTransform = {};

/**
 * Counter used to generate unique JSONP callback functions.
 * @private
 */
HGeoTransform.callbackCounter = new Date().getTime();

/**
 * Transforms a geographical point in coordinate system defined by SRID to
 * another coordinate system. EPSG is automatically used as authority. The
 * transformation is done using a backend call so this function should not
 * be used for on-the-fly transformations.
 *
 * Callback function is invoked with four arguments: targetPoint (HGeoPoint),
 * sourcePoint, sourceSRID, targetSRID.
 *
 * @param {HGeoPoint} sourcePoint
 * @param {Number} sourceSRID SRID of source coordinate system.
 * @param {Number} targetSRID SRID of target coordinate system.
 * @param {Function} callback Callback function invoked with result.
 * @static
 * @public
 * @see <a href="http://spatialreference.org/ref/epsg/">List of coordinate systems with SRID</a>
 * @example
 * function transformCallback(targetPoint, sourcePoint, sourceSRID, targetSRID) {
 *   alert("north: " + targetPoint.north + ", east: " + targetPoint.east);
 * }
 *
 * HGeoTransform.transformSRID(new HGeoPoint(7453389, 727060), 3021, 3006, transformCallback);
 */
HGeoTransform.transformSRID = function(sourcePoint, sourceSRID, targetSRID, callback) {
    // construct unique callback function name
    HGeoTransform.callbackCounter++;
    var callbackName = "sridJSONP" + HGeoTransform.callbackCounter;

    // create temporary callback function
    HGeoTransform[callbackName] = function(data) {
        var targetPoint = new HGeoPoint(data.x, data.y)
        callback(targetPoint, sourcePoint, sourceSRID, targetSRID);

        // free reference
        HGeoTransform[callbackName] = null;
    }

    // trigger JSONP call
    // @todo: change URL!
    var url = "http://192.168.10.115:8080/transform/resources/transform/" + sourceSRID + "/" + sourcePoint.north + "/" + sourcePoint.east + "?targetSRID=" + targetSRID + "&callback=" + "HGeoTransform." + callbackName;
    HMapHelpers.initJSONP(url);
};

/**
 * @fileOverview HMap utility classes.
 */

/**
 * @class A HPoint represents a point in pixels (however, it may be used in any way).
 *
 * @param {Number} x Horizontal x coordinate (increases to the right).
 * @param {Number} y Vertical y coordinate (icnreases downwards).
 *
 * @property {Number} x Horizontal x coordinate (increases to the right).
 * @property {Number} y Vertical y coordinate (increases downwards).
 * 
 * @constructor
 */
function HPoint(x, y) {
    this.x = x;
    this.y = y;
}

/**
 * Returns true if given point is equal to current.
 *
 * @param {HPoint} point Point to compare with.
 */
HPoint.prototype.equals = function(point) {
    return (this.x == point.x && this.y == point.y);
};

/**
 * Returns a new HPoint instance with same coordinates.
 * 
 * @returns {HPoint}
 */
HPoint.prototype.clone = function() {
    return new HPoint(this.x, this.y);
};

/**
 * @class HSize represents the size in pixels of a rectangular area.
 * 
 * @param {Number} width Width of rectangular area.
 * @param {Number} height Height of rectangular area.
 * 
 * @property {Number} width Width of rectangular area.
 * @property {Number} height Height of rectangular area.
 * 
 */
function HSize(width, height) {
    this.width = width;
    this.height = height;
}

/**
 * Returns true if sizes are equal.
 *
 * @param {HSize} size Size to compare with.
 */
HSize.prototype.equals = function(size) {
    return (this.width == size.width && this.height == this.height);
};

/**
 * @class A HPointRT90 represents a point in the RT90 coordinate system.
 * 
 * To avoid misunderstanding the RT90 x and y coordinates are instead labeled as
 * north and east.
 * 
 * @param {Number} north The RT90 x coordinate increases upwards.
 * @param {Number} east The RT90 y coordinate increases to the left.
 *
 * @property {Number} north The RT90 x coordinate increases upwards.
 * @property {Number} east The RT90 y coordinate increases to the left.
 * 
 * @constructor
 */
function HPointRT90(north, east) {
    north = parseFloat(north);
    east = parseFloat(east);

    // swap coordinates if wrong (implemented by request, be careful
    // since this hides errors that may appear elsewhere)
    if (east > 4000000 && north <= 4000000) {
        var temp = north;
        north = east;
        east = temp;
    }

    this.north = north;
    this.east = east;
}

/**
 * Returns true if given point is equal to current.
 *
 * @param {HPointRT90} rt90point Point to compare with.
 */
HPointRT90.prototype.equals = function(rt90point) {
    return (this.north == rt90point.north && this.east == rt90point.east);
};

/**
 * Get distance in meters between given point and this.
 *
 * @param {HPointRT90} rt90point Point to measure distance to.
 * @returns {Number} Distance in meters.
 */
HPointRT90.prototype.distanceTo = function(rt90point) {
    return Math.sqrt( Math.pow(this.east - rt90point.east, 2) + Math.pow(this.north - rt90point.north, 2) );
};

/**
 * Returns a new HPointRT90 instance with same coordinates.
 * 
 * @returns {HPointRT90}
 */
HPointRT90.prototype.clone = function() {
    return new HPointRT90(this.north, this.east);
};

/**
 * Returns a string with east and north value, in this order, separated by a comma.
 */
HPointRT90.prototype.toString = function() {
    return this.north + ", " + this.east;
};

/**
 * @class The HBoundsRT90 object represents a geographical rectangle in RT90
 * coordinates.
 *
 * Create the bounds object. Accepts zero, one or two points. If no point is
 * given when creating the object the HBoundsRT90.extend() function should be
 * used to set a bound. If only one point is given the bound will be equal to
 * that point.
 *
 * @param {HPointRT90} [p1] First point of bound.
 * @param {HPointRT90} [p2] Second point of bound.
 *
 * @property {HPointRT90} sw South west coordinate of bound.
 * @property {HPointRT90} ne North east coordinate of bound.
 *
 * @constructor
 */
function HBoundsRT90(p1, p2) {
    if (arguments.length == 0) {
        this.sw = null;
        this.ne = null;
    } else if (arguments.length == 1) {
        this.sw = p1.clone();
        this.ne = p1.clone();
    } else {
        var w, e, n, s;

        if (p1.east < p2.east) {
            w = p1.east;
            e = p2.east;
        } else {
            w = p2.east;
            e = p1.east;
        }

        if (p1.north < p2.north) {
            s = p1.north;
            n = p2.north;
        } else {
            s = p2.north;
            n = p1.north;
        }

        this.sw = new HPointRT90(s, w);
        this.ne = new HPointRT90(n, e);
    }
}

/**
 * Returns true if coordinates in given bound is equal to this bound.
 * 
 * @param {HBoundsRT90} bounds Bounds to compare.
 * @returns {Boolean} True if bounds are equal.
 */
HBoundsRT90.prototype.equals = function(bounds) {
    return (this.sw.equals(bounds.sw) && this.ne.equals(bounds.ne));
};

/**
 * Returns true if given RT90 point is within bounds.
 *
 * @param {HPointRT90} rt90point Point to check.
 * @returns {Boolean} True if within bounds.
 */
HBoundsRT90.prototype.containsRT90 = function(rt90point) {
    return (rt90point.north >= this.sw.north && rt90point.north <= this.ne.north) &&
           (rt90point.east >= this.sw.east && rt90point.east <= this.ne.east);
};

/**
 * Enlarge bound to contain given point.
 *
 * @param {HPointRT90} rt90point Point to include in rectangle.
 */
HBoundsRT90.prototype.extend = function(rt90point) {
    // manage special case when HBoundsRT90 is created with no initial point.
    if (this.ne === null || this.sw === null ) {
        this.sw = rt90point.clone();
        this.ne = rt90point.clone();
        return;
    }

    if (rt90point.north > this.ne.north) {
        // extend north
        this.ne.north = rt90point.north;
    } else if (rt90point.north < this.sw.north) {
        // extend south
        this.sw.north = rt90point.north;
    }

    if (rt90point.east > this.ne.east) {
        // extend east
        this.ne.east = rt90point.east;
    } else if (rt90point.east < this.sw.east) {
        // extend west
        this.sw.east = rt90point.east;
    }
};

/**
 * Returns a RT90 point whoose coordinates represent the size of the bound
 * (i.e. north is the height of the bound in meters, east is the width).
 * 
 * @returns {HPointRT90} Size of bound as RT90 point.
 */
HBoundsRT90.prototype.toSpan = function() {
    return new HPointRT90(this.ne.north - this.sw.north, this.ne.east - this.sw.east);
};

/**
 * Scale bound with factor while maintaining center position. Factor greater
 * than 1 makes bound larger, lesser than 1 makes it smaller.
 * 
 * @param {Number} factor Factor to scale with.
 */
HBoundsRT90.prototype.scale = function(factor) {
    var span = this.toSpan();

    // calculate padding in percent of bound size
    var horzPadding = Math.round(((span.east * factor) - span.east) / 2);
    var vertPadding = Math.round(((span.north * factor) - span.north) / 2);

    // extend/reduce bound with padding
    this.sw.north -= vertPadding;
    this.sw.east -= horzPadding;
    this.ne.north += vertPadding;
    this.ne.east += horzPadding;
};

/**
 * Get center RT90 coordinate of bounds rectangle.
 *
 * @returns {HPointRT90} Center of rectangle.
 */
HBoundsRT90.prototype.getCenter = function() {
    return new HPointRT90(Math.round((this.ne.north + this.sw.north) / 2), Math.round((this.ne.east + this.sw.east) / 2));
};

/**
 * This class is returned by HEvent.addListener() and HEvent.addDomListener(),
 * it is used with HEvent.removeListener() to remove an event listener.
 * 
 * @class Event listener handle used to identify an event listener to allow
 * it to be removed.
 */
var HEventListener = {
    source: null,
    eventType: null,
    handler: null,
    isDomEvent: false
};

/**
 * The HEvent namespace contains function to manage custom events and utility
 * function for DOM events. Use it to listen to custom events that is triggered
 * using HEvent.trigger().
 * 
 * @namespace
 */
var HEvent = {
    
    events: {},

    /**
     * Add event listener. The event handler will be called with this set to
     * the source object.
     *
     * @param {Object} source Source of events.
     * @param {String} eventType Event type.
     * @param {Function} handler Event handler.
     * @returns {HEventListener} Event listener handle used to remove event listener.
     */
    addListener: function(source, eventType, handler) {
        if (source == undefined) throw new Error("HEvent.addListener: source is undefined!");
        if (eventType == undefined) throw new Error("HEvent.addListener: eventType is undefined!");
        if (handler == undefined) throw new Error("HEvent.addListener: handler is undefined!");

        if (!source._listeners) {
            source._listeners = {};
        }

        if (!source._listeners[eventType]) {
            source._listeners[eventType] = [];
        }

        if (!source._listeners[eventType][handler]) {
            source._listeners[eventType].push(handler);
        }

        var eventListener = {
            source: source,
            eventType: eventType,
            handler: handler,
            isDomEvent: false
        };

        return eventListener;
    },

    /**
     * Add event listener to DOM event. The event handler will be called with
     * this set to the source object.
     *
     * @param {Object} source Element to listen to.
     * @param {String} eventType Type of event.
     * @param {Function} handler Function to handle event callback.
     * @returns {HEventListener} Event listener handle used to remove event listener.
     */
    addDomListener: function(source, eventType, handler) {
        if (source.addEventListener) {
            // Standard W3C DOM Events API
            if (eventType == "mousewheel") {
                source.addEventListener("DOMMouseScroll", handler, false);
            }
            source.addEventListener(eventType, handler, false);
        } else if (source.attachEvent) {
            // Internet Explorer API
            source.attachEvent("on" + eventType, handler);
        }

        var eventListener = {
            source: source,
            eventType: eventType,
            handler: handler,
            isDomEvent: true
        };

        return eventListener;
    },

    /**
     * Remove event listener installed using addListener or addDomListener.
     *
     * @param {HEventListener} el Event listener handle to remove.
     */
    removeListener: function(el) {
        if (el == null) return;
        
        if (!el.isDomEvent) {
            if (el.source._listeners && el.source._listeners[el.eventType]) {
                el.source._listeners[el.eventType].remove(el.handler);
            }
        } else {
            if(el.source.removeEventListener) {
                // Standard W3C DOM Events API
                if (el.eventType == "mousewheel") {
                  el.source.removeEventListener("DOMMouseScroll", el.handler, false);
                }
                el.source.removeEventListener(el.eventType, el.handler, false);
            } else if(el.source.detachEvent) {
                // Internet Explorer API
                el.source.detachEvent("on" + el.eventType, el.handler);
            }
        }
    },


    /**
     * Stop DOM event bubbling.
     *
     * @param {Object} event Event object.
     * @return {Object} Always returns false.
     */
    stopEvent: function(event) {
        var e = event || window.event;

        if (e.stopPropagation) e.stopPropagation();
        if (e.preventDefault) e.preventDefault();

        e.cancelBubble = true;
        e.returnValue = false;

        return false;
    },

    /**
     * Remove all event listeners in array that was installed using
     * addListener or addDomListener.
     *
     * @param {Array} elArr Array of HEventListener.
     */
    removeListeners: function(elArr) {
        for (var i = 0; i < elArr.length; i++) {
            HEvent.removeListener(elArr[i]);
        }
    },

    /**
     * Remove all listeners for given object and given event type.
     * Only custom events installed using addListener.
     *
     * @param {Object} source Source of events.
     * @param {String} eventType Event type.
     */
    clearListeners: function(source, eventType) {
        if (source._listeners && source._listeners[eventType]) {
            source._listeners[eventType] = [];
        }
    },
    
    /**
     * Remove all listeners for all event types for given object.
     * Only custom events installed using addListener.
     * 
     * @param {Object} source Source of events.
     */
    clearSource: function(source) {
        if (source._listeners) {
            source._listeners = null;
        }
    },

    /**
     * Returns true if there is a listener of given event type and source.
     * Only custom events installed using addListener.
     *
     * @returns {Boolean} True if there is a listener.
     */
    isListening: function(source, eventType) {
        return (source._listeners && source._listeners[eventType] && source._listeners[eventType].length > 0);
    },

    /**
     * Trigger an event. If the event handler returns false subsequent events are stopped.
     *
     * @param {Object} source Source of event.
     * @param {String} eventType Event type.
     * @param {...} [optional] Any number of optional arguments are passed
     *                 to event handler function when triggered.
     */
    trigger: function(source, eventType, optional) {
        if (source == undefined) throw new Error("HEvent.trigger: source is undefined!");
        if (eventType == undefined) throw new Error("HEvent.trigger: eventType is undefined!");

        if (source._listeners && source._listeners[eventType]) {
            var handlers = source._listeners[eventType];

            for (var i = handlers.length - 1; i >= 0; i--) {
                if (handlers[i] != undefined) {
                    // set this == source and pass on remaining arguments
                    if (false === (handlers[i]).apply(source, Array.prototype.slice.call(arguments, 2))) {
                        // stop event if function returns false
                        break;
                    }
                }
            }
        }
    }

};
/**
 * @fileOverview Static utility functions for development and debugging purposes.
 */

/**
 * Static utility functions for development and debugging purposes.
 * @namespace
 */
var HMapDev = {};

/**
 * Log message to debug area (textarea with id "mapDebug").
 * Note: If possible, use console.log() with FireBug instead.
 *
 * param {String} logString Message to log.
 */
HMapDev.log = function(logString) {
    // debug output hardcoded to TextArea element
    var debugTextArea = document.getElementById("mapDebug");
    
    if (debugTextArea) {
        var d = new Date();
        
        var h = d.getHours().toString();
        var m = d.getMinutes().toString();
        var s = d.getSeconds().toString();
        
        h = (h.length == 1 ? '0' : '') + h;
        m = (m.length == 1 ? '0' : '') + m;
        s = (s.length == 1 ? '0' : '') + s;
        
        // append string
        debugTextArea.value += "[" + h + ":" + m + ":" + s + "] " + logString + "\n";
        // automatically scroll to last line
        debugTextArea.scrollTop = debugTextArea.scrollHeight;
    }
};

/**
 * Assert that expression is true, else display alert message.
 *
 * param {Boolean} expr Expression to evaluate if true.
 * param {String} assertString Message to display if fail.
 */
HMapDev.assert = function(expr, assertString) {
    if (!expr) {
        alert("Assert: " + assertString);
    }
};

/**
 * Dump object properties as a string.
 *
 * param {Object} obj Object to echo.
 * return {String} String listing property:value of object.
 */
HMapDev.getObjectProperties = function(obj) {
    var str = "";

    for(prop in obj) {
        str += prop + ": " + obj[prop] + "\n";
    }    
    
    return str;
};

