// TomTom HD Traffic access library, $Revision: 38620 $

/// Following the Java conventions, the namespace for the tomtom HD Traffic service is com.tomtom.
var com;
if (!com) com = {};
else if (typeof com != 'object') throw new Error("com already exists and is not an object");

if (!com.tomtom) com.tomtom = {}
else if (typeof com.tomtom != 'object') throw new Error("com.tomtom already exists and is not an object");

/// Main access object is com.tomtom.Hdt
if (com.tomtom.Hdt) throw new Error("com.tomtom.Hdt already exists");
else com.tomtom.Hdt = {};

(function() {
    var mockImages = false;
    var mockJson = false;

    var logger = {
        debug: function() {}, info: function() {}, warn: function() {}, error: function() {}, fatal: function() {}
    }

    var imageType = 'png';
    /*@cc_on
      @if (@_jscript_version < 7)
        imageType = 'gif';
      @end
    @*/

    var discardElement = function(element) {}
    /*@cc_on
      @if (@_jscript_version <= 7)
        discardElement = function(element) {
            var garbageBin = document.getElementById('IELeakGarbageBin');
            if (!garbageBin) {
                   garbageBin = document.createElement('DIV');
                   garbageBin.id = 'IELeakGarbageBin';
                   garbageBin.style.display = 'none';
                   document.body.appendChild(garbageBin);
            }
            // move the element to the garbage bin
            garbageBin.appendChild(element);
            garbageBin.innerHTML = '';
        }
      @end
    @*/

    //
    // Helper functions
    //

    function isObject(obj, type) {
        return obj && typeof obj == 'object' && obj.constructor && obj.constructor == type;
    }

    //
    // Element methods
    //

    function deleteElement(element, container) {
        if (!element) return;
        while(element.hasChildNodes()) {
            if (container) {
                moveElement(element.firstChild, container);
            } else {
                deleteElement(element.firstChild);
            }
        }
        if (element.parentNode) {
            element.parentNode.removeChild(element);
        }
        discardElement(element);
    }

    function removeClassChildren(root, className, container) {
        var element = root.lastChild;
        while (element) {
            var tmp = element.previousSibling;
            if (isClass(element, className)) {
                deleteElement(element, container);
            }
            element = tmp;
        }
        element = root.lastChild;
    }

    function moveElement(element, newParent) {
        newParent = resolveIdOrElement(newParent)
        if (!element) return
        if (element.parentNode && element.parentNode == newParent) return;
        if (element.parentNode) element.parentNode.removeChild(element);
        newParent.appendChild(element);
    }

    function resolveIdOrElement(id) {
        if (typeof id == 'string') {
            return document.getElementById(id)
        } else {
            return id
        }
    }

    //
    // CSS Class helpers
    //

    function checkForScreenMedia(media) {
        if (media.length == 0) return true;
        for (var m= 0; m < media.length; ++m) {
            if (media == 'screen' || (media.item && media.item(m) == 'screen')) {
                return true;
            }
        }
        return false;
    }

    function getCSSProperty(className) {
        var sheets = document.styleSheets;
        var lastScreenSheet;
        for (var j = 0; j < sheets.length; ++j) {
            if (!checkForScreenMedia(sheets[j].media)) continue;
            try {
                var rules = null;
                rules = sheets[j].cssRules;
                if (!rules) rules = sheets[j].rules;
                for (var i = 0; i < rules.length; ++i) {
                    if (rules[i].selectorText == className) {
                        return rules[i].style
                    }
                }
                lastScreenSheet = sheets[j];
            } catch (NS_ERROR_DOM_SECURITY_ERR) {
                logger.error("NS_ERROR_DOM_SECURITY_ERR at sheet " + j)
            }
        }
        if (lastScreenSheet) {
            var rules = lastScreenSheet.cssRules;
            if (!rules) rules = lastScreenSheet.rules;
            var index = null
            if (lastScreenSheet.insertRule) {
                index = lastScreenSheet.insertRule(className + "{}", rules.length);
            } else {
                index = lastScreenSheet.addRule(className, "x-foo: none;", rules.length);
            }
            return getCSSProperty(className)
        } else {
            var styleSheetObj;
            var styleObj;
            if(document.createStyleSheet) {
                // Direkt neues Stylesheet anlegen (IE)
                styleSheetObj=document.createStyleSheet();
                styleObj = styleSheetObj.owningElement || styleSheetObj.ownerNode;
            } else {
                styleObj = document.createElement("style");
                document.getElementsByTagName("head")[0].appendChild(styleObj);
            }
            styleObj.setAttribute("type","text/css");
            /*@cc_on
            @if(@_jscript)
                styleSheetObj.addRule(className, "x-foo: none;");
            @else @*/
                try {
                    styleObj.innerHTML = className + " {}"
                } catch (e) {
                    styleObj.appendChild(document.createTextNode(className + "{}\n"));
                }
            /*@end @*/
            return getCSSProperty(className)
        }
    }
    com.tomtom.getCSSStyle = getCSSProperty;


    function changeClassProperty(className, id, value) {
        var style
        if (typeof className == 'string') {
            style = getCSSProperty(className)
        } else {
            style = className
        }
        var prop = style[id]
        if (prop) {
            if (value) {
                style[id] = value
            }
            else {
                style[id] = ""
            }
        } else if (value) {
            style[id] = value
        }
        return style;
    }

    com.tomtom.setCSSProperty = changeClassProperty;

    function isClass(element, c) {
        var classes = element.className;
        if (!classes) return false;
        if (classes == c) return true;
        return classes.search("\\b" + c + "\\b") != -1;
    }
    com.tomtom.isClass = isClass;

    function addClass(element, className) {
        if (element.className) {
            if (!isClass(element, className)) {
                element.className += " " + className;
            }
        } else {
            element.className = className;
        }
    }
    com.tomtom.addClass = addClass;

    function removeClass(element, className) {
        if (element.className) {
            if (element.className == className) element.className = null;
            else {
                var rex = new RegExp("\\b" + className + "\\b\\s*");
                element.className = element.className.replace(rex, "")
            }
        }
    }
    com.tomtom.removeClass = removeClass;

    // @param a: array or object
    // @return a, if a is an array, [a] otherwise
    function arraze(a) {
        if (!isObject(a, Array)) {
            return [a];
        }
        return a;
    }

    // Convenient function: Adds an element to an array.
    // @param a: array or object
    // @return an array starting with a, ending with element.
    function pushBack(a, element) {
        var rv = arraze(a);
        rv[rv.length] = element;
        return rv;
    }

    // To avoid rounding problems with different browser versions
    // we crop the coordinates by hand
    function cropCoordinate(coo) {
        var tmp = coo.toFixed(8);
        var separator = /([+-]{0,1}\d+)\.(\d{1,7})\d*/
        separator.exec(tmp.toString());
        return RegExp.$1 + "." + RegExp.$2;
    }

    // Returns the bounding box of a tile.
    // @param tile: tile number
    // @param tileSize: Size of a single tile
    // @param width: Number of tiles
    // @return the coordinates of the bounding box for this tile
    var bbox = function(tile, tileSize, width) {
        return cropCoordinate(tile[0] * tileSize) + ","
            + cropCoordinate((tile[1] - width) * tileSize) + ","
            + cropCoordinate((tile[0] + width) * tileSize) + ","
            + cropCoordinate((tile[1]) * tileSize)
    };

    // Transforms a screen coordinate to Mercartor coordinate
    // @param tile: tile number
    // @param pixel: offset in pixels within this tile
    // @param tileSize: size of a single tile
    // @return coordinate in 900913 mercartor coordinate system
    var transformCoordinate = function(tile, pixel, tileSize) {
        return cropCoordinate((tile + pixel / 256.0) * tileSize);
    }

    // Transforms a screen point to a Mercartor point
    // @param tiles: array of tile numbers
    // @param point: array of coordinate offsets
    // @param tileSize: size of a single tile
    // @return coordinate in 900913 mercartor coordinate system
    var transformCoordinates = function(tiles, point, tileSize) {
        var rv = []
        for (var i = 0; i < point.length; ++i) {
            // we need to alter the sign of point coordinates
            rv[i] = transformCoordinate(tiles[i], (1 - 2 * (i % 2)) * point[i], tileSize)
        }
        return rv
    }

    // Transforms a Latitude/Longitude pair into Mercator coordinates
    // @returns Mercator coordinates x=geo[0], y=geo[1]
    var computeMercCoord = function(latitude,longitude) {
        var geo = []
        geo[0] = 6378137 * longitude * Math.PI /180;
        geo[1] = 6378137 * Math.log(Math.tan(Math.PI *((latitude + 90)/360)));
        return geo;
    }

    // Compute pixel offsets wrt the viewpoint
    var computeOffsets = function(region, mercatorPoint) {
        v = transformCoordinates(region.tile, region.offset, region.tileSize);
        var offset=[];
        offset[0] =  (mercatorPoint[0] - v[0]) /region.tileSize *256;
        offset[1] = -(mercatorPoint[1] - v[1]) /region.tileSize *256; 
        return offset;
    }

    // Computes the (zoom relative) coordinates of the bounding box of a region.
    // @param region
    // @return the bounding box coordinates on a full pixel image of the earth
    //         in the zoom level of the region. That is, coordinates are pixels
    //         from aequator/0-meridian.
    function bboxCoordinates(region) {
        var offset = [region.viewport.x, region.viewport.y]
        if (region.viewport.origin) {
            offset[0] = transformOrigin(region.viewport.origin.x, offset[0], region.numberTiles.x);
            offset[1] = transformOrigin(region.viewport.origin.y, offset[1], region.numberTiles.y);
        }
        if (region.viewport.align) {
            offset[0] = viewportAlign(region.viewport.align.x, offset[0], region.size.x);
            offset[1] = viewportAlign(region.viewport.align.y, offset[1], region.size.y);
        }
        var rv = [offset[0] + region.tiles.x * 256, region.tiles.y * 256 - offset[1]]
        return [rv[0], rv[1], rv[0] + region.size.x, rv[1] - region.size.y]
    }

    function vectorFoo(v, w, f) {
      if (f == null) {
        f = w
        w = {x: null, y: null}
      }
      if (typeof f == 'function') {
        return { x: f(v.x, w.x), y: f(v.y, w.y) }
      } else {
        return { x: f.x(v.x, w.x), y: f.y(v.y, w.y) }
      }
    }
    function handlePort(nb, port, scale) {
      if (port) {
        if (port == 'center') { return nb * (scale / 2) }
        else if (port == 'right' || port == 'bottom') { return nb * scale }
      }
      return 0
    }
    var xyPlus = { x: function(a,b) { return a + b }, y: function(a,b) { return a - b } }

    function changePerspective(origin, perspective, scale) {
        if (perspective) {
            return vectorFoo(origin, perspective,
                function(nb, port) { return handlePort(nb, port, scale)}
            )
        } else {
            return {x:0, y:0}
        }
    }

    function absCoordinates(definition) {
        var abs = vectorFoo(definition.tiles, definition.viewport, 
          { x: function(a,b) { return a * 256 + b }, y: function(a,b) { return a * 256 - b } })
        var delta = changePerspective(definition.numberTiles, definition.viewport.origin, 256)
        return vectorFoo(abs, delta, xyPlus)
    }

    function logCoordinates(coo, msg) {
        logger.info((msg ? msg + ": " : "") + coo.x + "/" + coo.y)
    }

    function resizeDefinition(definition, size) {
        var abs = absCoordinates(definition)
        var zoomCorrection = vectorFoo(size, definition.size,
          function(a,b) {
            var rv = 0;
            while (a > (b << (rv + 1)) ) ++rv
            if (rv > 0) return rv;
            while (a < (b >> -(rv - 1)) ) --rv
            return rv;
          }
        )
        logCoordinates(zoomCorrection)
        zoomCorrection = (zoomCorrection.y < zoomCorrection.x) ? zoomCorrection.y : zoomCorrection.x
        if (zoomCorrection > 0) {
          abs = vectorFoo(abs, function(x) { return x << zoomCorrection })
        } else if (zoomCorrection < 0) {
          abs = vectorFoo(abs, function(x) { return x >> -zoomCorrection })
        }
        logCoordinates(abs)
        var newDef = {}
        var left = vectorFoo(abs,
            changePerspective(size, definition.viewport.align, 1),
            {x: function(a,b) { return a - b }, y: function(a,b) { return a + b }})
        newDef.tiles = vectorFoo(left, {x:0, y: 1}, function(a,b) { return (a >> 8) + b})
        newDef.tiles.zoomLevel = definition.tiles.zoomLevel - zoomCorrection;
        newDef.numberTiles = {x: size.x >> 8, y: size.y >> 8}
        newDef.viewport = {x: (left.x - (newDef.tiles.x << 8)), y: (newDef.tiles.y << 8) - left.y }
        newDef.size = size;
        return newDef;
    }

    // Comutes intersection of a region with main region.
    // Method is _not_ symmetric!
    // @param lhs: main region
    // @param rhs: intersecting region
    // @return null if rhs does not intersect lhs or the intersection is lhs.
    //      pixel coordinates of intersection within lhs, otherwise.
    function intersect(lhs, rhs) {
        if (!rhs || !rhs.tiles) return null;
        if (lhs.tiles.zoomLevel < rhs.tiles.zoomLevel) return null;
        var lhsBbox = bboxCoordinates(lhs);
        var rhsBbox = bboxCoordinates(rhs);
        for (var i = 0; i < rhsBbox.length; ++i) {
            rhsBbox[i] = Math.floor(rhsBbox[i] / (1 << (lhs.tiles.zoomLevel - rhs.tiles.zoomLevel)));
        }
        if (rhsBbox[0] < lhsBbox[0]) rhsBbox[0] = lhsBbox[0];
        if (rhsBbox[1] > lhsBbox[1]) rhsBbox[1] = lhsBbox[1];
        if (rhsBbox[2] > lhsBbox[2]) rhsBbox[2] = lhsBbox[2];
        if (rhsBbox[3] < lhsBbox[3]) rhsBbox[3] = lhsBbox[3];
        if (rhsBbox[0] >= rhsBbox[2] || rhsBbox[1] <= rhsBbox[3]) return null;
        if (rhsBbox[0] == lhsBbox[0] && rhsBbox[1] == lhsBbox[1] && rhsBbox[2] == lhsBbox[2] && rhsBbox[3] == lhsBbox[3])
            return null
        return [rhsBbox[0] - lhsBbox[0], lhsBbox[1] - rhsBbox[1], rhsBbox[2]- lhsBbox[0], lhsBbox[1] - rhsBbox[3]]
    }

    // Bounding box for a region.
    // @param region Contains a region object with tile, tileSize, and offset
    // @param size Size of canvas in pixels
    // @return bounding box of the region
    var regionBox = function(region, size) {
		if ( region.offset ) {
			var o1 = region.offset;
			var o2 = [o1[0] + size[0], o1[1] + size[1]];
			return transformCoordinates(region.tile, o1, region.tileSize).toString() + ","
				+ transformCoordinates(region.tile, o2, region.tileSize).toString();
		}
		return "0,0";
    };

    // URL for retrieving the incidents
    // @param callback: name of the (global) function to be called after loading
    // @param bbox: bounding box for incidents
    // @param zoomLevel: level of details
    // @param baseUrl: URL prefix
    // @param apiKey: API key for this URL
    // @param language: The localization
    // @note If mockJson is set, the URL will be a relative path
    // @return a URL for this request
    var incidentsUrl = function(callback, bbox, ccode, zoomLevel, baseUrl, apiKey, language) {
        if (mockJson) {
            if (typeof mockJson == "string") return mockJson;
            return "json/" + bbox + ".js"
        }
            return baseUrl + "/lbs/services/traffic/pois/" + bbox
            + (ccode ? "," + ccode + ":" : "")
            + "/" + zoomLevel + "/" + language + "/json," + callback + "($)"
            + "/" + apiKey;
    };

    // Returns a URL for retrieving the top 10 list
    var top10Url = function(callback, country, baseUrl, apiKey) {
        return baseUrl + "/lbs/services/traffic/top10/" + country
            + "/json," + callback + "($)" + "/" + apiKey;
    };

    // Returns a URL for retrieving the Country overview object
    var CountryOverviewUrl = function(callback, country, baseUrl, apiKey) {
        return baseUrl + "/lbs/services/traffic/overview/" + country
            + "/json," + callback + "($)" + "/" + apiKey;
    };

    // URL for retrieving a tile.
    // @note lbs.tomtom.com only supports certain bounding boxes. For each zoom level
    //       the used mercartor coordinate system (900913) is subdivided into squares
    //       of the same size. (A zoom level is a subdivision of the previous zoom
    //       level). A bounding box must describe such a square. The tile returned
    //       will be 256x256.
    // @param layer: name of the layer
    // @param bbox: bounding box of the tile
    // @param baseUrl: URL prefix
    // @param apiKey: API key for this URL
    var tileUrl = function(layer, bbox, baseUrl, apiKey,imageType1) {
        if (mockImages) {
            return "gfx/" + layer + "/" + bbox + ".gif"
        }
        return baseUrl + "/lbs/wms/" + (new Date()).getTime() + "?" +
            "LAYERS=" + layer + "&" +
            "REQUEST=GetMap&FORMAT=image%2F" + ( imageType1 || imageType ) + "&" +
            "APIKEY=" + apiKey +
            "&SERVICE=WMS&VERSION=1.1.1&STYLES=&EXCEPTIONS=application%2Fvnd.ogc.se_inimage&" +
            "SRS=EPSG%3A900913&" +
            "BBOX=" + bbox +
            "&WIDTH=256&HEIGHT=256";
    };

    // ID of an incident
    function incidentId(mainId, nb) {
        return mainId + "-incident-" + nb;
    }

    // ID of a layer
    function layerId(hdtId, layer) {
        return hdtId + "-" + layer;
    }

    //
    // Element creators
    //

    // Creates new element or does nothing.
    // If the document already contains an element of ID id, only this
    // element is returned.
    // Otherwise, an element is created and passed to function 'exercise'.
    // @note Insertion into the DOM tree is not done by this function!
    // @param id of the Element
    // @param type of the object to be generated 
    // @param exercise function applied to the new element, if created
    // @return the elemnt with ID id.
    function newElementOrNothing(id, type, exercise, stamp) {
        var element = document.getElementById(id);
        if (element) {
            if (stamp && element.getAttribute('x-hdt-stamp') == stamp) {
                return element;
            }
            deleteElement(element);
            element = null;
        }
        element = document.createElement(type);
        element.id = id;
        exercise(element);
        return element;
    }

    function createCurtain(element) {
        element.style.visibility = 'hidden';
        return {
            hidden: element,
            waitFor: function(id) {
                if (!this.curtains) this.curtains = 0;
                ++this.curtains;
            },
            done: function(id) {
                --this.curtains;
                if (this.curtains < 1) {
                    element.style.visibility = 'visible';
                }
            }
        }
    }

    // Creates a tile image.
    // @param layer: layer name
    // @param tile: array of tile numbers for the image
    // @param tileSize: size of one tile
    // @param serviceData: information about lbs.tomtom.com access point
    // @return an IMG element with the correct URL.
    function createImage(id, layer, tile, tileSize, serviceData, curtain) {
        var img = document.createElement('img');
        curtain.waitFor(id);
        img.onload = function() { curtain.done(id); }
        img.id = id
        img.setAttribute('src', tileUrl(layer, bbox(tile, tileSize, 1),
            serviceData.baseUrl(), serviceData.apiKey, layer == 'basic' ? 'png' : imageType));
        return img;
    }

    // Creates a div containing a tile (stitching the images)
    // This div wraps an IMG element for positioning the images next to each other.
    // @param id: ID of this div
    // @param layer: layer name
    // @param tile: array of tile numbers for the image
    // @param offset: array representing the tile number of this tile within a tiling
    // @param tileSize: size of one tile
    // @param serviceData: information about lbs.tomtom.com access point
    // @return a DIV element with absolute position containing the IMG element.
    function createImageDiv(id, layer, tile, offset, tileSize, serviceData, curtain) {
        return newElementOrNothing(id, 'div', function(div) {
            div.style.position = "absolute";
            div.style.left = Math.floor(offset[0] * 256) + "px";
            div.style.top = Math.floor(offset[1] * 256) + "px";
            div.appendChild(createImage(id + '-img-' + layer, layer,
                [tile[0] + offset[0], tile[1] - offset[1]], tileSize, serviceData, curtain));
        });
    }

    // Creates a div containing all tiles (positioning the image).
    // This div wraps all image DIV's. This allows positioning the images within
    // a surrounding div.
    // @param id: ID of this div
    // @param layer: layer name
    // @param region: full region information
    // @param serviceData: information about lbs.tomtom.com access point
    // @param nbTiles: array of the number of tiles in x and y direction
    // @return a DIV element with absolute position shifted left and up, containing all tiles.
    function createTileDiv(id, layer, region, serviceData, nbTiles, curtain) {
        return newElementOrNothing(id, 'div', function(div) {
            div.style.position = 'absolute';
            div.style.left = Math.floor(-region.offset[0]) + "px";
            div.style.top = Math.floor(-region.offset[1]) + "px";
            for (var i = 0; i < nbTiles[0]; i++) {
                for (var j = 0; j < nbTiles[1]; j++) {
                    div.appendChild(createImageDiv(id + "-" + i  + "-" + j,
                        layer, region.tile, [i, j], region.tileSize, serviceData, curtain));
                }
            }
        });
    }

    // Creating a div for cropping.
    // This div crops away image parts not fitting into visible region.
    // @param id: ID of this div
    // @param layer: layer name
    // @param region: full region information
    // @param size: array of width/height of the resulting image.
    // @param serviceData: information about lbs.tomtom.com access point
    // @param nbTiles: array of the number of tiles in x and y direction
    // @return a DIV element hiding everything not fitting into width/height.
    //         Contains tiles.
    function createCropDiv(id, layer, region, size, serviceData, nbTiles) {
        return newElementOrNothing(id, 'div', function(div) {
            var curtain = createCurtain(div);
            div.style.overflow = 'hidden';
            div.style.width = Math.floor(size[0]) + "px";
            div.style.height = Math.floor(size[1]) + "px";
            div.style.position = "absolute";
            div.appendChild(createTileDiv(id + "-tiles", layer, region, serviceData, nbTiles, curtain));
        });
    }

    //
    // Icon mapping
    //

    // Maps categories to numbers.
    var categoryMapping = {
        'ACCIDENT': '01',
        'FOG': '02',
        'DANGEROUS_CONDITIONS': '03',
        'RAIN': '04',
        'ICE': '05',
        'JAM': '06',
        'LANE_CLOSED': '07',
        'ROAD_CLOSURE': '08',
        'ROAD_WORK': '09',
        'WIND': '10',
        'FLOODING': '11',
        'DETOUR': '12',
        'CLUSTER': '13'
    };
    // Translates icon type to icon id.
    function findIconType(category) {
        if (!categoryMapping[category]) return '00';
        return categoryMapping[category];
    }

    // URL for an icon (on the LBS server)
    function lbsUrlGenerator(base, iconType, stacked) {
        return base + "/lbs/images/traffic/traffic-" + findIconType(iconType) + '.gif';
    }

    //
    // Load info in background
    //

    function processTrafficInformation(hdt, traffic) {
        if (!traffic || !traffic.traffic) return;
        hdt.stamp = (new Date()).getTime();
        hdt.traffic = {};
        logger.info("Process traffic information " + traffic.traffic.timestamp);
        hdt.traffic.timestamp = traffic.traffic.timestamp;
        hdt.traffic.overview = traffic.traffic.overview;
        hdt.traffic.overview.ageMs = traffic.traffic.ageMs;
        hdt.traffic.overview.timestamp = traffic.traffic.timestamp;
        hdt.traffic.roads = {}
        var count = 0;
        var length = 0;
        var delay = 0;
        if (traffic.traffic.incidents) {
            if (traffic.traffic.icons &&
                hdt.config && hdt.config.tiles && hdt.config.tiles.zoomLevel < 10)
            {
                hdt.traffic.icons = arraze(traffic.traffic.icons.icon);
            } else {
                hdt.traffic.icons = [];
            }
            hdt.traffic.incidents = [];
            var incidentMap = {};
            var incidents = arraze(traffic.traffic.incidents.incident);
            for (var i = 0; i < incidents.length; ++i) {
                if (-1 != incidents[i].countryIso.search(
                                new RegExp('^' + hdt.countryIso + '|' + hdt.countryIso + '$')))
                {
                    var tmp = incidents[i];
                    if (tmp.tmcDescription) {
                        tmp.primaryLocation = tmp.tmcDescription.primaryLocation;
                        tmp.primaryLocationType = tmp.tmcDescription.primaryLocationType;
                        tmp.secondaryLocation = tmp.tmcDescription.secondaryLocation;
                        tmp.secondaryLocationType = tmp.tmcDescription.secondaryLocationType;
                        tmp.roadFrom = tmp.tmcDescription.roadFrom;
                        tmp.roadTo = tmp.tmcDescription.roadTo;
                        tmp.roadNumber = tmp.tmcDescription.roadNumber;
                        tmp.roadName = tmp.tmcDescription.roadName;
                        tmp.tmcDescription = null;
                    } else {
                        tmp.roadNumber = tmp.roadName;
                        tmp.roadName = null;
                    }
                    if (tmp.roadNumber == null 
                        && tmp.roadName != null) {
                        logger.info(tmp.roadName + " without road number");
                        if( hdt.roadNumbering != null
                            && hdt.roadNumbering.namedRoads != null)
                        {                        
                            tmp.roadNumber = hdt.roadNumbering.namedRoads[tmp.roadName];
                            logger.info(hdt.roadName + ": injected road number " + tmp.roadNumber);
                            if (tmp.roadNumber == "") {
                                tmp.roadNumber = tmp.roadName;
                                tmp.roadName = null
                            }
                        }
                    }
                    incidentMap[incidents[i].id] = i;
                    if (tmp.primaryLocation && tmp.secondaryLocation) {
                        hdt.traffic.incidents = pushBack(hdt.traffic.incidents, tmp);
                        ++count;
                        length += tmp.lengthMeters;
                        delay += tmp.delaySeconds;
                        if (hdt.traffic.roads[tmp.roadNumber] == null) {
                            hdt.traffic.roads[tmp.roadNumber] = { className: 'roadNumber-' + count, count: 0 }
                        }
                        hdt.traffic.roads[tmp.roadNumber].count += 1
                        tmp.roadNumberClass = hdt.traffic.roads[tmp.roadNumber].className
                    } else {
                        logger.warn("remove incident " + incidents[i].id + ":" +
                            (tmp.primaryLocation ? "" : " no primary loc") +
                            (tmp.secondaryLocation ? "" : " no secondary loc"))
                    }
                } else {
                    logger.warn("remove incident " + incidents[i].id +
                        ": " + incidents[i].countryIso + " != " + hdt.countryIso)
                }
            }
            hdt.traffic.overview.visible = { 'count': count, totalLengthMeters: length, totalDelaySeconds: delay }
            for (var i = 0; i < hdt.traffic.icons.length; ++i) {
                var icon = hdt.traffic.icons[i]
                var iconIds = arraze(icon.incidents.id);
                var iconIncidents = []
                for (var j = 0; j < iconIds.length; ++j) {
                    var mappedId = incidentMap[iconIds[j]];
                    if (mappedId != null && incidents[mappedId] != null) {
                        iconIncidents = pushBack(iconIncidents, incidents[mappedId]);
                    }
                }
                icon.incidents.incidents = iconIncidents;
            }
        }
        hdt.createOverview(hdt.traffic.overview);
    }

    function processTop10(hdt, top10) {
        hdt.top10 = top10.top10;
        hdt.createTop10Overview({ timestamp: hdt.top10.timestamp });
    }

    function processCountryOverview(hdt, overview) {
        hdt.countryOverview = overview.trafficOverview;
        hdt.createCountryOverview(hdt.countryOverview);
    }

    function executePending(pending, name) {
        var pendingArray = arraze(pending[name])
        for (var i = 0; i < pendingArray.length; ++i) {
            pendingArray[i]();
        }
        pending[name] = null;
    }

    function addCallback(hdt, name, processor) {
        var callbackId = "callback-" + name + "-" + hdt.id;
        // This is the callback function.
        window[callbackId] = function(json) {
            hdt.processJson(hdt, json, processor);
            // Call all pending functions...
            if (hdt.pending && hdt.pending[name]) {
                executePending(hdt.pending, name);
            }
            --hdt.pendingLoad;
            if (hdt.pendingLoad == 0) {
                if (hdt.pending && hdt.pending.last) {
                    executePending(hdt.pending, 'last');
                }
                if (hdt.onLoaded) {
                    hdt.onLoaded();
                }
            }
            // Reset callback state
            window[callbackId] = null;
            hdt.isLoading[name] = false;
        }
        return callbackId;
    }

    // Loads incidents.
    // A global callback function is created on the fly. This function
    // will be called after the incidents are loaded. The function then
    // will load info into hdt and call all pending functions.
    function getJSONRequest(hdt, name, processor, urlGenerator) {
        // Load only once
        if (hdt.isLoading && hdt.isLoading[name]) return;
        if (!hdt.isLoading) hdt.isLoading = {};
        hdt.isLoading[name] = true;
        if (!hdt.pendingLoad) hdt.pendingLoad = 0;
        ++hdt.pendingLoad;
        var callbackId = addCallback(hdt, name, processor);
        var url = urlGenerator("window['" + callbackId + "']", hdt) + "?" + (new Date()).getTime();
        // Add the script element
        var script = document.getElementById(callbackId);
        if (script) {
            // We need to remove the old element, first.
            deleteElement(script);
            script = null;
        }
        script = document.createElement('script');
        script.id = callbackId;
        script.setAttribute('charset', 'utf-8')
        script.setAttribute('src', url);
        script.setAttribute('type', 'text/javascript');
        // Now, find the right place...
        var place = document.getElementsByTagName('head');
        if (place && place.length > 0)  place = place[0]
        else place = document.body;
        // Add script element...
        place.appendChild(script);
    }

    function loadIncidents(hdt) {
        getJSONRequest(hdt, 'traffic', processTrafficInformation, function(id, hdt) {
            return incidentsUrl(id, regionBox(hdt.region, hdt.size), hdt.countryIso,
                hdt.region.zoomLevel,
                hdt.serviceData.baseUrl(), hdt.serviceData.apiKey, hdt.language || 'en');
        });
    }

    function loadTop10(hdt) {
        getJSONRequest(hdt, 'top10', processTop10, function(id, hdt) {
            return top10Url(id, hdt.country || 'nl',
                hdt.serviceData.baseUrl(), hdt.serviceData.apiKey);
        });
    }

    function loadCountryOverview(hdt) {
        getJSONRequest(hdt, 'overview', processCountryOverview, function(id, hdt) {
            return CountryOverviewUrl(id, hdt.country || 'nl',
                hdt.serviceData.baseUrl(), hdt.serviceData.apiKey);
        });
    }


    // Executes a function or postpones it.
    // If hdt.traffic is defined, the function is executed imediately.
    // Otherwise the function is added to the pending stack and
    // a loadIncidents is issued.
    // @param hdt: the main object
    // @param fun: function to execute
    function executeOrPostpone(hdt, name, fun, loadFunction) {
        if (hdt[name]) fun();
        else if (name == 'last' && (!hdt.pendingLoad)) {
            fun();
        }
        else {
            if (hdt.pending && hdt.pending[name]) {
                hdt.pending[name] = pushBack(hdt.pending[name], fun);
            } else {
                if (!hdt.pending) hdt.pending = {}
                hdt.pending[name] = fun;
            }
            loadFunction(hdt);
        }
    }

    //
    // Region definition
    //
    var regionLookup = {};
    var menu = null
    var hierarchy = null
    var overview = null

    function setRegions(lookup) {
        if (lookup.menu) {
            menu = lookup.menu
        } else {
            menu = null
        }
        if (lookup.hierarchy) {
            hierarchy = lookup.hierarchy
        } else {
            hierarchy = null;
        }
        if (lookup.overview) {
            overview = lookup.overview
        } else {
            overview = null;
        }
        regionLookup = lookup;
        
    }

    function addRegion(name, config) {
        regionLookup[name] = config;
    }

    var TileSize = 305.748113140705;
    var lookupRegion = function(region) {
        var tmp = regionLookup[region];
        if (!tmp) tmp = region;
        if (isObject(tmp,Array)) {
            return { tiles: {x:tmp[0], y:tmp[1], zoomLevel: tmp[2]},
                viewport: {x: tmp[3], y:tmp[4]} };
        }
        return tmp;
    }

    //
    // High level functions
    //

    function createCopyrightLayer(hdt, name, bottom, left) {
        return newElementOrNothing(layerId(hdt.id, name), 'img', function(img) {
            img.style.zIndex = '100';
            if (bottom) {
                img.style.bottom = "0px";
            } else {
                img.style.top = "0px";
            }
            if (left) {
                img.style.left = "0px";
            } else {
                img.style.right = "0px";
            }
            img.style.position = "absolute";
            img.src = hdt.serviceData.baseUrl() + "/lbs/images/traffic/logos/" + name + "." + imageType;
        });
    }

    function createScaleImage(hdt, img, bbox) {
        var o1 = hdt.computeOffsets(bbox[0], bbox[1])
        var o2 = hdt.computeOffsets(bbox[2], bbox[3])
        var capsula = document.createElement('div');
        capsula.style.overflow = 'hidden';
        capsula.style.width = Math.floor(hdt.size[0]) + "px";
        capsula.style.height = Math.floor(hdt.size[1]) + "px";
        capsula.style.position = 'relative';
        var crop = document.createElement('div');
        capsula.appendChild(crop);
        if (typeof img == 'string') {
            var tmp = document.createElement('img');
            tmp.src = img;
            img = tmp;
        }
        img.style.width = Math.floor(o2[0] - o1[0]) + 'px';
        img.style.height = Math.floor(o2[1] - o1[1]) + 'px';
        crop.style.left = Math.floor(o1[0]) + 'px';
        crop.style.top = Math.floor(o1[1]) + 'px';
        crop.style.position = 'absolute'
        crop.appendChild(img);
        return capsula;
    }

    //
    // Settings helper
    //

    function viewportAlign(align, offset, size) {
        if (!align) return offset;
        if (align == 'center') {
            return Math.floor(offset - size / 2);
        } else if (align == 'right' || align == 'bottom') {
            return offset - size;
        }
        return offset;
    }

    function transformOrigin(origin, offset, nbTiles) {
        if (!origin) return offset;
        if (origin == 'center') {
            return nbTiles * 128 + offset;
        } else if (origin == 'right' || origin == 'bottom') {
            return nbTiles * 256 + offset;
        } else if (origin == 'left' || origin == 'top') {
            return offset;
        } else {
            return origin + offset;
        }
    }

    function setConfig(hdt, config) {
        if (!hdt.config) { hdt.config = {}; }
        if (config.size) {
            hdt.config.size = config.size;
        }
        if (hdt.config.size) {
            if (hdt.config.size.x < 130) {
                hdt.config.size.x = 130;
            }
            hdt.size = [hdt.config.size.x, hdt.config.size.y];            
        }
        if (config.tiles) {
            hdt.config.tiles = config.tiles;
        }
        if (hdt.config.tiles) {
            if (!hdt.region) hdt.region = {};
            hdt.region.tile = [hdt.config.tiles.x, hdt.config.tiles.y]
            hdt.region.zoomLevel = (17 - hdt.config.tiles.zoomLevel)
            hdt.region.tileSize = (1 << hdt.config.tiles.zoomLevel) * TileSize
        }
        if (config.viewport) { hdt.config.viewport = config.viewport; }
        if (config.numberTiles) {
            hdt.config.numberTiles = config.numberTiles
        }
        
        if (hdt.config.viewport) {
            var offset = [hdt.config.viewport.x, hdt.config.viewport.y]
            if (hdt.config.viewport.origin) {
                offset[0] = transformOrigin(hdt.config.viewport.origin.x, offset[0], hdt.tileSize[0]);
                offset[1] = transformOrigin(hdt.config.viewport.origin.y, offset[1], hdt.tileSize[1]);
            }
            if (hdt.config.viewport.align) {
                offset[0] = viewportAlign(hdt.config.viewport.align.x, offset[0], hdt.size[0]);
                offset[1] = viewportAlign(hdt.config.viewport.align.y, offset[1], hdt.size[1]);
            }
            hdt.region.offset = offset
        }
        // calculate number tiles
        if (hdt.region != null && hdt.region.offset != null) {
            hdt.tileSize = [Math.ceil((hdt.region.offset[0] + hdt.size[0]) / 256), 
                Math.ceil((hdt.region.offset[1] + hdt.size[1]) / 256)];
        }
    }

    function checkId(hdt, id) {
        if (typeof id == 'string') {
            hdt.baseId = id;
            return null;
        }
        return id;
    }

    function mkNewMouseover(mouseover, element) {
        return function() {
            moveElement(mouseover(), element)
        }
    }

    // Moves mouse over to another layer
    // @param img the image element
    // @param base the other layer
    // @param capsula the container for the image
    function alterMouseOver(img, base, capsula) {
        if (img.onmouseover) {
            var mirror = createPositionElement(0, 0);
            mirror.className = capsula.className;
            mirror.style.bottom = capsula.style.bottom;
            mirror.style.left = capsula.style.left;
            mirror.id = capsula.id + '-mirror';
            mirror.style.zIndex = 1000;
            base.appendChild(mirror);
            img.onmouseover = mkNewMouseover(img.onmouseover, mirror);
            mirror = null;
        }
    }

    function createPositionElement(x, y) {
        var element = document.createElement('div');
        element.style.position = "absolute";
        element.style.width = Math.floor(x) + 'px';
        element.style.height = Math.floor(y) + 'px';
        return element;
    }

    function addIcons(hdt, id, base, root, iconCreator) {
        if (!hdt.traffic || !hdt.traffic.icons || !hdt.traffic.incidents) return;

        var icons = hdt.traffic.icons;
        for (var i = 0; i < icons.length; ++i) {
            var capsula = createPositionElement(0, 0);
            capsula.className = id;
            capsula.id = id + '-capsula-' + i
            capsula.style.bottom = icons[i].y + "px";
            capsula.style.left = icons[i].x + "px";
            var img = iconCreator(icons[i], capsula);
            alterMouseOver(img, base, capsula);
            capsula.appendChild(img);
            root.appendChild(capsula);
            capsula = null;
            img = null;
        }
    }

    function createMouseActiveCanvas(hdt, map) {
        var div = createPositionElement(hdt.size[0], hdt.size[1]);
        div.style.overflow = 'hidden';
        if (map) {
            var img = document.createElement('img');
            // TODO configure URL
            img.src = '../examples/gfx/map_t.gif';
            img.useMap = "#" + map.id;
            img.border = '0';
            div.appendChild(img);
        }
        return div;
    }

    function createRegionMouseover(changeRegion, region) {
        return function() {
            changeRegion(region);
        }
    }

    function regionMouseover(region, area, mirror) {
        var timeout
        area.onmouseover = function(event) {
            timeout = window.setTimeout(function() {
                addClass(area, 'hdtHighlitedRegion')
                addClass(mirror, 'hdtHighlitedRegion')
            }, 500)
        }
        area.onmouseout = function() {
            window.clearTimeout(timeout);
            removeClass(area, 'hdtHighlitedRegion');
            removeClass(mirror, 'hdtHighlitedRegion')
        }
    }

    function defineRegionArea(hdt, root, mother, region, changeRegionHandle, map) {
        if (regionLookup[region] == null) return;
        logger.debug(hdt.regionName + " -- " + region);
        if (region == hdt.regionName) return;
        var rg = intersect(hdt.config, regionLookup[region]);
        if (!rg) return;
        var area;
        var name = regionName(region, hdt.language)
        if (!name) return;
        if (map) {
            area = document.createElement('area');
            area.setAttribute('nohref', 'nohref')
            area.setAttribute('shape', 'rect');
            area.setAttribute('title', name);
            area.setAttribute('alt', name);
            area.setAttribute('coords', rg);
        } else {
            var width = Math.floor(rg[2] - rg[0]);
            area = createPositionElement(rg[2] - rg[0], rg[3] - rg[1]);
            area.style.left = rg[0] + 'px';
            area.style.top = rg[1] + 'px';
            addClass(area, 'hdtRegion');
            var mirror = createPositionElement(0, 0);
            mirror.className = area.className;
            mirror.style.left = area.style.left;
            mirror.style.top = area.style.top;
            mirror.style.zIndex = 1000;
            root.appendChild(mirror);
            if (!changeRegionHandle.title) {
                area.title = name
            } else {
                var n = document.createElement('div')
                n.className = 'tooltip'
                n.style.width = Math.floor(width < 200 ? 200 : width) + 'px'
                changeClassProperty('.tooltip', 'display', 'none');
                addClass(n, 
                    (2 * rg[0] < hdt.size[0]) ? 'left' : 'right');
                addClass(n,
                    (2 * rg[1] < hdt.size[1] && rg[3] < hdt.size[1] - 20) ? 'bottom' : 'top');
                var span = document.createElement('span')                
                n.appendChild(span)
                if (typeof changeRegionHandle.title == 'function') {
                    span.innerHTML = changeRegionHandle.title(name);
                } else {
                    span.innerHTML = changeRegionHandle.title
                }
                mirror.appendChild(n)
            }
            regionMouseover(name, area, mirror);
        }
        if (changeRegionHandle.setter) {
            area.onclick = createRegionMouseover(changeRegionHandle.setter, region);
        } else {
            area.onclick = createRegionMouseover(changeRegionHandle, region);
        }
        if (mother.firstChild) {
            mother.insertBefore(area, mother.firstChild);
        } else {
            mother.appendChild(area);
        }
    }
    function createRegionCanvas(hdt, root, mother, changeRegionHandle, map) {
        if (map) {
            mother = map;
        } else if (!mother) {
            mother = createMouseActiveCanvas(hdt);
        }
        if (hierarchy && hdt.regionName && hierarchy[hdt.regionName]) {
            var regions = hierarchy[hdt.regionName]
            for (var i = 0; i < regions.size(); ++i) {
                defineRegionArea(hdt, root, mother, regions[i], changeRegionHandle, map)
            }
        } else {
            for (var region in regionLookup) {
                defineRegionArea(hdt, root, mother, region, changeRegionHandle, map)
            }
        }
        return mother;
    }

    function createBaseCanvas(id) {
        return {
            hidden: true,
            id: id,
            hide: function() {
                changeClassProperty('#' + this.id, 'display', 'none');
            },
            show: function(noForce) {
                if (!document.getElementById(this.id)) this.add();
                changeClassProperty("#" + this.id, 'display', null);
            },
            clear: function() {
                deleteElement(document.getElementById(this.id));
            },
            reload: function() {
                this.clear();
                this.add();
            }
        }
    }

    function createExternalSourceCanvas(mainDiv, createFunction) {
        var canvas = createBaseCanvas(mainDiv.id + "-" + (new Date()).getTime());
        canvas.add = function () {
            if (this.id && document.getElementById(this.id)) return;
            var cl = createFunction();
            var div = document.createElement('div');
            div.style.position = 'absolute';
            div.style.zIndex = this.index;
            div.id = this.id
            mainDiv.appendChild(div);
            moveElement(cl, div);
        }
        return canvas;
    }

    function createCustomCanvas(mainDiv, customLayer) {
        var createFunction;
        if (typeof customLayer == 'function') {
            createFunction = customLayer;
        } else {
            createFunction = function() {
                var element = resolveIdOrElement(customLayer)
                return element.cloneNode(true)
            };
        }
        return createExternalSourceCanvas(mainDiv, createFunction);
    }

    function createTubesCanvas(mainDiv, hdt) {
        var canvas = createBaseCanvas(layerId(hdt.id, 'traffic'));
        canvas.add = function() {
            if (this.id && document.getElementById(this.id)) return;
            var traffic = createCropDiv(layerId(hdt.id, 'traffic'), 'traffic',
                    hdt.region, hdt.size, hdt.serviceData, hdt.tileSize);
            traffic.style.zIndex = this.index;
            this.id = traffic.id;
            mainDiv.appendChild(traffic);
        }
        return canvas;
    }

    function delayedAddIcons(hdt, id, root, overDiv, iconHandle) {
        executeOrPostpone(hdt, 'traffic',
            function() { addIcons(hdt, id, root, overDiv, iconHandle) },
            loadIncidents
        );
    }

    function createIconCanvas(hdt, root, overDiv, iconHandle) {
        var canvas = createBaseCanvas(hdt.id + "-icon");
        canvas.show = function() {
            if (!this.active) this.add();
            changeClassProperty("." + this.id, 'display', 'block');            
        }
        canvas.hide = function() {
            changeClassProperty("." + this.id, 'display', 'none');
        }
        canvas.add = function() {
            if (iconHandle && !this.active && !document.getElementById(this.id)) {
                delayedAddIcons(hdt, this.id, root, overDiv, iconHandle);
            }
            this.active = true
        }
        canvas.clear = function() {
            if (!this.id) return;
            var locus = document.getElementById(this.id + '-bin');
            if (locus == null) {
                locus = document.createElement('div');
                locus.id = this.id + '-bin';
                locus.style.display = 'none';
                document.body.appendChild(locus);
            }
            removeClassChildren(root, this.id, locus);
            removeClassChildren(overDiv, this.id);
            this.active = null;
        }
        var lateLoad = setNull(this, 'last', loadIncidents)
        canvas.reload = function() {
            this.clear();
            lateLoad(this);
        }
        return canvas;
    }

    function addCanvas(hdt, index, canvas) {
        canvas.index = index;
        hdt.canvas = pushBack(hdt.canvas, canvas);
    }

    function createMainDiv(hdt, config) {
        var mainDiv = document.getElementById(hdt.id + "-images");
        if (mainDiv) {
            // XXX clear mainDiv
        } else if (hdt.size) {
            mainDiv = createPositionElement(hdt.size[0], hdt.size[1])
            mainDiv.style.position = 'relative';
            mainDiv.id = hdt.id + "-images";
            mainDiv.className = "hdt-image-frame";
        }

        hdt.canvas = [];
        var index = 1;

        // Traffic tubes
        addCanvas(hdt, index++, createTubesCanvas(mainDiv, hdt));

        if (config.customLayer) {
            var customLayers = arraze(config.customLayer);
            for (var i = 0; i < customLayers.length; ++i) {
                addCanvas(hdt, index++, createCustomCanvas(mainDiv, customLayers[i]));
            }
        }

        // mouse active layer
        var map = resolveIdOrElement(config.useMap);
        var mouseActive = createMouseActiveCanvas(hdt, map);
        mouseActive.style.zIndex = index++;
        mainDiv.appendChild(mouseActive);

        addCanvas(hdt, null, createIconCanvas(hdt, mainDiv, mouseActive, config.iconHandle));

        if (config.changeRegionHandle) {
            createRegionCanvas(hdt, mainDiv, mouseActive, config.changeRegionHandle, map);
        }

        // Branding
        ++index;
        if (hdt.size[0] > 120) {
            var c = createCopyrightLayer(hdt, 'copyright', true, true);
            c.style.zIndex = index;
            mainDiv.appendChild(c);
            if (hdt.size[0] > 260) {
                var link = document.createElement('a');
                link.href = "http://www.tomtom.com";
                link.style.zIndex = index;
                link.appendChild(createCopyrightLayer(hdt, 'tt_hd', true, false));
                link.firstChild.style.border = 0;
                mainDiv.appendChild(link);
            }
        }
        return mainDiv;
    }

    // TODO handle error
    // TODO delayed drawing if traffic information is needed.
    function createImageCanvas(hdt, config) {
        var root = resolveIdOrElement(config.root);
        var mainDiv = createMainDiv(hdt, config);
        var canvas = {
            show: function(layer) {
                if (!mainDiv || !mainDiv.parentNode) this.add();
                if (!layer) mainDiv.style.display = 'block';
                else if (layer == 'tubes') {
                    hdt.canvas[0].show();
                } else if (layer == 'icons') {
                    hdt.canvas[hdt.canvas.length - 1].show(true);
                } else hdt.canvas[layer].show(true);
            },
            hide: function(layer) {
                if (!mainDiv) return;
                if (!layer) mainDiv.style.display = 'none';
                else if (layer == 'tubes') hdt.canvas[0].hide();
                else if (layer == 'icons') hdt.canvas[hdt.canvas.length - 1].hide();
                else hdt.canvas[layer].hide();
            },
            add: function() {
                if (!mainDiv) mainDiv = createMainDiv(hdt, config);
                var basic = createCropDiv(layerId(hdt.id, 'basic'), 'basic',
                        hdt.region, hdt.size, hdt.serviceData, hdt.tileSize);
                basic.style.zIndex = 0;
                for (var i = 0; i < hdt.canvas.length; ++i) {
                    hdt.canvas[i].add();
                }
                mainDiv.appendChild(basic);
                root.appendChild(mainDiv);
            },
            clear: function(layer) {
                if (!mainDiv) return;
                if (!layer) {
                    deleteElement(mainDiv);
                    mainDiv = null;
                }
                else if (layer == 'tubes') hdt.canvas[0].clear();
                else if (layer == 'icons') hdt.canvas[hdt.canvas.length - 1].clear();
                else hdt.canvas[layer].clear();
            },
            reload: function(layer) {
                if (!layer) {
                    for (var i = 0; i < hdt.canvas.length; ++i) {
                        hdt.canvas[i].reload();
                    }
                }
                else if(layer == 'tubes') hdt.canvas[0].reload();
                else if (layer == 'icons') hdt.canvas[hdt.canvas.length - 1].reload();
                else hdt.canvas[layer].reload();
                
            },
            getCanvas: function(layer) {
                if (!layer) return this;
                else if (layer == 'tubes') return hdt.canvas[0];
                else if (layer == 'icons') return hdt.canvas[hdt.canvas.length - 1];
                else return hdt.canvas[layer];
            }
        }
        return canvas;
    }

    Hdt.prototype.createScaledImage = function(img, bbox) {
        return createScaleImage(this, img, bbox);
    }

    function createTextCanvas(id, getIncidents, resetIncidents, config) {
        var canvas = {
            root: resolveIdOrElement(config.root),
            hide: function() {
                if (!this.id) return;
                changeClassProperty("." + this.id, 'display', 'none');
            },
            show: function() {
                if (!this.id) this.add()
                changeClassProperty("." + this.id, 'display', null);
            },
            add: function() {
                this.incidents = arraze(getIncidents());
                if (this.order) this.incidents.sort(this.order);
                this.id = id + "-text";
                for (var i = 0; i < this.incidents.length; ++i) {
                    var incident = this.incidents[i];
                    if (incident) {
                        var element = config.handleIncident(incident);
                        if (element) {
                            addClass(element, this.id);
                            element.setAttribute("x-hdt-id", incident.id);
                            this.root.appendChild(element);
                        }
                    }
                }
            },
            clear: function() {
                if (!this.id) return;
                removeClassChildren(this.root, this.id);
                this.id = null;
            },
            reload: function() {
                this.clear();
                resetIncidents(this);
            },
            sort: function(order) {
                this.order = order;
                var idMap = {}
                var incidents = this.incidents;
                if (isObject(incidents, Array)) {
                    for (var i = 0; i < incidents.length; ++i) {
                        if (incidents[i]) idMap[incidents[i].id] = i
                    }
                }
                var children = []
                var element = this.root.firstChild;
                while (element) {
                    var tmp = element.nextSibling;
                    if (isClass(element, this.id)) {
                        children = pushBack(children, element);
                    }
                    this.root.removeChild(element);
                    element = tmp;
                }
                children.sort(
                    function(a,b) {
                        lhs = a.getAttribute("x-hdt-id");
                        rhs = b.getAttribute("x-hdt-id");
                        return order(incidents[idMap[lhs]], incidents[idMap[rhs]]);
                    }
                )
                for (var i = 0; i < children.length; ++i) {
                    this.root.appendChild(children[i])
                }
            }
        }
        if (config.order) canvas.order = config.order;
        return canvas;
    }

    Hdt.prototype.changeImageCanvas = function(config) {
        if (!this.size) this.size = [500,400]
        this.imageCanvas = createImageCanvas(this, config);
        return this.imageCanvas;
    }

    function setNull(hdt, prop, load) {
        return function(canvas) {
            hdt[prop] = null
            executeOrPostpone(hdt, prop, function() { canvas.add() }, load);
        };
    }

    function incidentLookup(hdt) {
        return function() { return hdt.traffic.incidents; }
    }
    Hdt.prototype.changeIncidentsCanvas = function(config) {
        this.incidentsCanvas = createTextCanvas(this.id + "-incidents",
            incidentLookup(this), 
            setNull(this, 'traffic', loadIncidents),
            config);
        return this.incidentsCanvas;
    }

    function incidentLookup2(hdt, type) {
        return function() {
            if (hdt.top10 && hdt.top10['by' + type]) {
                return hdt.top10['by' + type].incident;
            }
        }
    }
    Hdt.prototype.changeTop10Canvas = function(type, config) {
        if (!this.top10Canvas) this.top10Canvas = {}
        this.top10Canvas[type] = createTextCanvas(this.id + "-" + type,
            incidentLookup2(this, type),
            setNull(this, 'top10', loadTop10),
            config);
        return this.top10Canvas[type];
    }

    Hdt.prototype.changeOverviewBehaviour = function(config) {
        if (config.country) {
            this.createCountryOverview = config.country;
            if (this.countryOverview) {
                this.createCountryOverview(this.countryOverview);
            } else {
                this.loadCountryOverview();
            }
        }
        if (config.incidents) {
            this.createOverview = config.incidents;
            if (this.traffic && this.traffic.overview) {
                this.createOverview(this.traffic.overview);
            } else {
                this.loadIncidents();
            }
        }
        if (config.top10) {
            this.createTop10Overview = config.top10;
            if (this.top10) {
                this.createTop10Overview(this.top10.timestamp);
            } else {
                this.loadTop10List();
            }
        }
    }

    Hdt.prototype.display = function() {
        if (this.incidentsCanvas) {
            var c = this.incidentsCanvas;
            executeOrPostpone(this, 'traffic', function() { c.add() }, loadIncidents);
        }
        if (this.top10Canvas) {
            var canvas = this.top10Canvas;
             executeOrPostpone(this, 'top10', function() {
                     for (var type in canvas) {
                         canvas[type].add();
                     }
                 },
                 loadTop10
             )
        }
        if (this.imageCanvas) executeOrPostpone(this, 'last', this.imageCanvas.add, function() { } );
    }

    // XXX Overviews!
    // imageCanvas: { root:, customLayer:, useMap:, changeRegionHandle:, iconHandle }
    // incidentsCanvas: { root:, handleIncident:, order:}
    // top10Canvas: { Length: {}, Duration: {}}
    Hdt.prototype.setCanvases = function(config) {
        if (config.imageCanvas) {
            this.changeImageCanvas(config.imageCanvas);
        } else if (this.imageCanvas) {
            this.imageCanvas.clear();
            this.imageCanvas = null;
        }
        if (config.incidentsCanvas) {
            this.changeIncidentsCanvas(config.incidentsCanvas)
        } else if (this.incidentsCanvas) {
            this.incidentsCanvas.clear();
            this.incidentsCanvas = null;
        }
        if (this.top10Canvas) {
            for (var type in this.top10Canvas) {
                this.top10Canvas[type].clear();
            }
            this.top10Canvas = null;
        }
        if (config.top10Canvas) {
            for (var type in config.top10Canvas) {
                this.changeTop10Canvas(type, config.top10Canvas[type])
            }
        }
        if (config.overview) {
            this.changeOverviewBehaviour(config.overview)
        }
        if (config.roadNumbering) {
            this.roadNumbering = config.roadNumbering
        }
    }

    Hdt.prototype.getCanvas = function(path) {
        if (path == null) {
            return { imageCanvas: this.imageCanvas, incidentsCanvas: this.incidentsCanvas,
                top10Canvas: this.top10Canvas }
        } else {
            var paths = path.split('.');
            if (paths[0] == 'image') {
                return this.imageCanvas.getCanvas(paths[1])
            } else if (paths[0] == 'incidents') {
                return this.incidentsCanvas
            } else if (paths[0] == 'top10') {
                return this.top10Canvas[paths[1]]
            }
        }
    }

    // sort incidences
    // for all incidences
    //     if consider
    //        add to root
    //
    // Definition of the HDT object...
    //
    Hdt.prototype.computeOffsets = function(latitude,longitude) {
        return computeOffsets(this.region,computeMercCoord(latitude,longitude));
    };

    Hdt.prototype.loadIncidents = function() { loadIncidents(this); };
    Hdt.prototype.loadTop10List = function() { loadTop10(this); };
    Hdt.prototype.loadCountryOverview = function() { loadCountryOverview(this); };

    Hdt.prototype.reload = function() {
        delete hdt.traffic
        if (this.imageCanvas) {
            this.imageCanvas.reload();
        }
        if (this.incidentsCanvas) {
            this.incidentsCanvas.reload();
        }
        if (this.top10Canvas) {
            for (var type in this.top10Canvas) {
                this.top10Canvas[type].reload();
            }
        }
    }

    Hdt.prototype.clear = function() {
        if (this.imageCanvas) {
            this.imageCanvas.clear();
        }
        if (this.incidentsCanvas) {
            this.incidentsCanvas.clear();
        }
        if (this.top10Canvas) {
            for (var type in this.top10Canvas) {
                this.top10Canvas[type].clear();
            }
        }
    }

    //
    // Hooks
    //

    Hdt.prototype.createOverview = function() {}
    Hdt.prototype.createCountryOverview = function() {}
    Hdt.prototype.createTop10Overview = function() {}

    // Settings

    Hdt.prototype.setConfig = function(config) { setConfig(this, config); }
    Hdt.prototype.setLayerBaseId = function(id) { this.baseId = id; };
    Hdt.prototype.setLanguage = function(lang) { this.language = lang; };

    Hdt.prototype.setCountry = function(country) {
        this.country = country.toLowerCase();
        this.countryIso = country.toUpperCase();
        // ISO 3166 A3 codes
        switch ( this.country ) {
            case 'nl':
                this.countryIso = 'NLD';
                break;
            case 'gb':
            case 'uk':
                this.countryIso = 'GBR';
                break;
            case 'de':
                this.countryIso = 'DEU';
                break;
            case 'fr':
                this.countryIso = 'FRA'
                break;
            case 'ch':
                this.countryIso = 'CHE'
                break;
        }
        if (com.tomtom.Hdt[country]) {
            if (com.tomtom.Hdt[country].units) {
                com.tomtom.Hdt.converter.units = com.tomtom.Hdt[country].units
            }
            if (com.tomtom.Hdt[country].length) {
                com.tomtom.Hdt.converter.length = com.tomtom.Hdt[country].length
            }
            if (com.tomtom.Hdt[country].defaultRegion) {
                this.defaultRegion = com.tomtom.Hdt[country].defaultRegion
            }
        }
    };

    Hdt.setRegions = function(regions) { setRegions(regions); };
    Hdt.getRegions = function() {
        var rv = [];
        for (var region in regionLookup) {
            // XXX This needs clean up: The region is a mess by now!
            if (region != 'menu' && region != 'hierarchy' && region != 'overview') {
                var tmp = [region];
                if (regionLookup[region].name) {
                    tmp = pushBack(tmp, regionLookup[region].name);
                } else {
                    tmp = pushBack(tmp, region);
                }
                rv = pushBack(rv, tmp);
            }
        }
        return rv;
    }

    function regionName(region, language) {
        var rv = regionLookup && regionLookup[region] && regionLookup[region].name ? regionLookup[region].name : region
        if (language
            && regionLookup.translations
            && regionLookup.translations[language]
            && regionLookup.translations[language][rv]
        ) {
            return regionLookup.translations[language][rv]
        }
        return rv;
    }

    function optionList(root, options, selected, language) {
        for (var i = 0; i < options.length; ++i) {
            if (regionLookup[options[i]]) {
                var opt = document.createElement('option');
                var name = regionName(options[i], language);
                opt.innerHTML = name;
                opt.value = options[i];
                if (selected != null && opt.value == selected) {
                    opt.selected = "selected";
                }
                root.appendChild(opt);
            } else {
                logger.warn("Can't find region " + options[i])
            }
        }
    }
    Hdt.fillRegionMenu = function(selector, selected, translate) {
        if (menu) {
            for (var cat in menu) {
                if (cat == '') {
                    optionList(selector, menu[cat], selected, translate != null ? translate.language : null)
                } else {
                    var optGroup = document.createElement('optgroup');
                    optGroup.label = translate != null && translate[cat] ? translate[cat] : cat
                    optionList(optGroup, menu[cat], selected, translate != null ? translate.language : null);
                    selector.appendChild(optGroup);
                }
            }
        } else {
            optionList(selector, Hdt.getRegions(), selected, translate != null ? translate.language : null)
        }
    }

    Hdt.addRegion = function(name, config) { addRegion(name, config); };

    Hdt.prototype.setTileSize = function(tiles) { 
        setConfig(this, { numberTiles: { x: tiles[0], y: tiles[1] } });
    }
    Hdt.prototype.setRegion = function(region) {
        logger.debug("Region: " + region)
        this.regionName = region;
        setConfig(this, lookupRegion(region));
    };
    Hdt.prototype.setRegionOverview = function() {
		if (overview) setConfig(this, overview);
    };
    Hdt.prototype.region = function() {
        return regionName(this.regionName, hdt.language);
    }
    Hdt.prototype.setSize = function(width, height) {
        if (this.config) {
            setConfig(this, resizeDefinition(this.config, {x: width, y: height}))
        } else {
            setConfig(this, {size: {x : width, y: height}})
        }
    };
    Hdt.prototype.setRoadNumbering = function(numbering) {
        this.roadNumbering = numbering;
    }

    // Debug

    // This is an indirection only for logging.
    Hdt.prototype.processJson = function(hdt, json, processor) {
        processor(hdt, json);
    }

    com.tomtom.doMock = function(mock) { mockJson = mock ? mock : true; }

    com.tomtom.setLogger = function(l) { logger = l }
    com.tomtom.getLogger = function() { return logger }

    com.tomtom.dispose = function(element) { discardElement(element) };

    Hdt.prototype.doMock = function() { mockJson = true; }
    Hdt.prototype.getTileSize = function() { return this.tileSize; };
    Hdt.prototype.bbox = function() { return exBbox(this.region, this.size); }
    Hdt.prototype.offset = function(id) { if (id == 'x') return this.region.offset[0]; else return this.region.offset[1]; }
    Hdt.prototype.coordinate = function(x,y) { return transformCoordinates(this.region.tile, [x, y], this.region.tileSize); }
    Hdt.prototype.info = function() { return {region: this.region, size: this.size}; }
    Hdt.prototype.getConfig = function() { return this.config; };

    //
    // Hdt constructor
    //

    function Hdt(url, key) {
        this.serviceData = {apiKey: key}
        if (isObject(url, Array)) {
            var pos = 0;
            this.serviceData.baseUrl = function() {
                var rv = url[pos];
                pos = (pos + 1) % url.length;
                return rv;
            }
        } else {
            this.serviceData.baseUrl = function() {
                return url;
            }
        }
        this.tileSize = [3,3];
		this.defaultRegion = 'default';
        this.setCountry('nl');
        var id = "com-tomtom-hdt-";
        var nb = 0;
        while (document.getElementById(id + nb)) {
            ++nb;
        }
        this.id = id + nb;
        var idDiv = document.createElement('div');
        idDiv.id = this.id;
        idDiv.style.display = 'none';
        document.body.appendChild(idDiv);
    }

    com.tomtom.Hdt = Hdt;

    function convert(nb, rate, unit, threshold, asNumber) {
        var value = Math.round(nb / rate)
        if (asNumber) {
            return value;
        } else {            
            return nb > threshold ? value + unit : null
        }
    }
    
    com.tomtom.Hdt.converter = {
        units: {length: 'km', time: 'min'},
        rate: {length: 1000, time: 60},
        length: function(nb, asNumber) {
            return convert(nb, this.rate.length, this.units.length, 0, asNumber)
        },
        time: function(nb, trunk, asNumber) {
            return convert(nb, this.rate.time, this.units.time, trunk ? 0 : 30, asNumber)
        }
    }

    logger.debug('loaded HDT');
})();


