WaniKani Dashboard Cockpit

Complete alternate dashboard style for the basic panels

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         WaniKani Dashboard Cockpit
// @namespace    rwesterhof
// @version      0.7.5
// @description  Complete alternate dashboard style for the basic panels
// @include      /^https:\/\/(www|preview)\.wanikani\.com(\/(#)?dashboard)?(\/)?$/
// @run-at       document-end
// @grant        none
// @license      GPL-3.0-or-later
// ==/UserScript==

(function() {
    'use strict';

    /* global $, wkof */

    if (!window.wkof) {
        let response = confirm('WaniKani Dashboard Cockpit script requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.');

        if (response) {
            window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
        }

        return;
    }

    wkof.include('Menu,Settings,ItemData');
    wkof.ready('Menu,Settings')
        .then(load_settings)
        .then(install_menu)
        .then(add_css)
        .then(createPanels)
        .then(createPanelContents)
        .then(displayCache)
        .then(wkof.ready('ItemData').then(fetch_and_update));

    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    //-------------------------------------------------------------------LOGIC AND RULES-------------------------------------------------------------------//
    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    var CACHE_VERSION = '0.2';
    var CACHE_KEY = 'db_cockpit_cache';
    function displayCache() {
        var cached_json = localStorage.getItem(CACHE_KEY);
        if (cached_json) {
            var cached = JSON.parse(cached_json);
            if (cached.version == CACHE_VERSION) {
                lockedCount = cached.lockedCount;
                update_display(cached.itemBreakdown);
            }
            else if (cached.version == '0.1') {
                cached.itemBreakdown.allPassed = false;
                lockedCount = cached.lockedCount;
                update_display(cached.itemBreakdown);
            }
        }
    }

    function fetch_and_update() {
        fetch_locked_items()
            .then(processLockedItems)
            .then(fetch_assignment_items)
            .then(constructItemBreakdown)
            .then(update_display);
    }

    function update_display(itemBreakdown) {
        repositionMainPanel();
        loadSizes();
        clearCanvas();
        drawSRSProgress(itemBreakdown);
        drawSRSProgressDetail(itemBreakdown);
        drawSRSProgressLeechCounts(itemBreakdown);
        drawSRSProgressLevel(itemBreakdown);
        // works on global var because of dual wkof retrieve
        createLockedCount(itemBreakdown);
        createTotalProgressIndication(itemBreakdown);
        displayGrid(itemBreakdown.allPassed);
        reOffsetCanvas();

        // fires a custom event to signal cockpit loaded
        const event = document.createEvent('Event');
        event.initEvent("dashboard-cockpit-loaded", true, true);
        $('#cockpitPanel')[0].dispatchEvent(event);
    }

    // Fetches the relevant items
    function fetch_assignment_items() {
        var config = {
            wk_items: {
                options: { review_statistics: true, assignments: true },
			    filters: { srs: '0,1,2,3,4,5,6,7,8,9' }
			}
		};
		return wkof.ItemData.get_items(config);
	}

    function fetch_locked_items() {
        const config = {
            wk_items: {
                options: { assignments: true },
                filters: { srs: '-1' }
            }
        };
        return wkof.ItemData.get_items(config);
    }

    // filtering by srs abbrev (App|Mas|Guru1|...)
	function filterBySRSCountOnAbbrev(counts, abbrev) {
		return counts.filter(function(countItem) {
            return countItem.name.slice(0, abbrev.length) == abbrev;
        });
	}

    var lockedCount = { name: "Loc", total: 0, radical: 0, kanji: 0, vocabulary: 0 };
    const SUBJECT_TYPE_MAP = { 'radical': 'radical', 'kanji': 'kanji', 'vocabulary': 'vocabulary', 'kana_vocabulary': 'vocabulary' };
    function processLockedItems(data) {
        const type = wkof.ItemData.get_index(data, 'item_type');
        lockedCount = { name: "Loc", total: 0, radical: 0, kanji: 0, vocabulary: 0 };
        lockedCount.total = data.length;
        Object.keys(SUBJECT_TYPE_MAP).forEach(itemType => {
            var mappedType = SUBJECT_TYPE_MAP[itemType];
            lockedCount[mappedType] += (type[itemType]||[]).length;
        });
    }

    // helper function to construct the counts overview objects
    function constructBreakdown() {
        return {
            total: 0,
            progressScore: 0,
            bySRSArray: [
                { name: "Loc", total: 0, radical: 0, kanji: 0, vocabulary: 0, leech: 0 },
                { name: "Ini", total: 0, radical: 0, kanji: 0, vocabulary: 0, leech: 0 },
                { name: "Appr1", total: 0, radical: 0, kanji: 0, vocabulary: 0, leech: 0 },
                { name: "Appr2", total: 0, radical: 0, kanji: 0, vocabulary: 0, leech: 0 },
                { name: "Appr3", total: 0, radical: 0, kanji: 0, vocabulary: 0, leech: 0 },
                { name: "Appr4", total: 0, radical: 0, kanji: 0, vocabulary: 0, leech: 0 },
                { name: "Guru1", total: 0, radical: 0, kanji: 0, vocabulary: 0, leech: 0 },
                { name: "Guru2", total: 0, radical: 0, kanji: 0, vocabulary: 0, leech: 0 },
                { name: "Mas", total: 0, radical: 0, kanji: 0, vocabulary: 0, leech: 0 },
                { name: "Enl", total: 0, radical: 0, kanji: 0, vocabulary: 0, leech: 0 },
                { name: "Bur", total: 0, radical: 0, kanji: 0, vocabulary: 0, leech: 0 }
            ]
        };
    }

    // constants for the intervals
    var srsConstants = {
        duration: [ 0, 14400, 28800, 82800, 169200, 601200, 1206000, 2588400, 10364400, 0 ],
        stageWeight: [ 0, 1, 3, 5, 7, 10, 14, 20, 28, 36 ],
        stageProgressWeight: [ 0, 1, 1, 1, 1, 2, 2, 4, 4, 0 ]
    }

    // run past the list of items (data) to produce the various counts we need
    function constructItemBreakdown(data) {
        var itemBreakdown = {
            level: 0,
            allPassed: true, // start optimistically
            countBySRS: constructBreakdown(),
            countByLevel: []
        };

        var now = new Date();
        // get the setting, but enforce range 1-4
        var leechThreshold = Math.min(4, Math.max(1, wkof.settings.db_cockpit.leechThreshold));
        var leechMinWrong = Math.min(10, Math.max(1, wkof.settings.db_cockpit.leechMinWrong));

        var processItem = function(item) {
            var level = item.data.level;
            if (!itemBreakdown.countByLevel[level]) {
                itemBreakdown.countByLevel[level] = constructBreakdown();
            }

            if (item.assignments) {
                var srsStage = item.assignments.srs_stage;

                itemBreakdown.countByLevel[level].bySRSArray[srsStage+1][SUBJECT_TYPE_MAP[item.assignments.subject_type]]++;
                itemBreakdown.countByLevel[level].bySRSArray[srsStage+1].total++;
                itemBreakdown.countBySRS.bySRSArray[srsStage+1][SUBJECT_TYPE_MAP[item.assignments.subject_type]]++;
                itemBreakdown.countBySRS.bySRSArray[srsStage+1].total++;

                // calculate progress score for this item
                var stageScore = srsConstants.stageWeight[item.assignments.srs_stage];
                if (item.assignments.available_at != undefined) {
                    stageScore += srsConstants.stageProgressWeight[srsStage] * (1 - (Math.max(((new Date(item.assignments.available_at) - now)), 0) / (1000 * srsConstants.duration[srsStage])));
                }
                itemBreakdown.countByLevel[level].progressScore += stageScore;

                // check for leech status
                if (isLeech(item, leechThreshold, leechMinWrong)) {
//                    console.log(\"Item \" + item.data.characters + \" identified as leech due to (meaning, reading) (\" + item.review_statistics.meaning_incorrect + \", \" + item.review_statistics.reading_incorrect + \") wrong answers\");
                    itemBreakdown.countBySRS.bySRSArray[srsStage+1].leech++;
                }

                // check for passed
                itemBreakdown.allPassed = itemBreakdown.allPassed && (item.assignments.passed == undefined ? item.assignments.passed_at != null : item.assignments.passed);
            }
            else {
                // for locked items we have no information on type - this will need to be retrieved separately to display locked detail info
                itemBreakdown.countByLevel[level].bySRSArray[0].total++;
                itemBreakdown.countBySRS.bySRSArray[0].total++;
                itemBreakdown.allPassed = false;
            }
            itemBreakdown.countByLevel[level].total++;
            itemBreakdown.countBySRS.total++;
		}

        data.forEach(processItem);

        // and grab the user's current level
        itemBreakdown.level = $('li.user-summary__attribute a')[0].href.split('/level/')[1];

        // cache the results for next page load
        var json = { version: CACHE_VERSION, itemBreakdown: itemBreakdown, lockedCount: lockedCount };
        localStorage.setItem(CACHE_KEY, JSON.stringify(json));
        return itemBreakdown;
    }

	function isLeech(item, threshold, leechMinWrong) {
		if (item.review_statistics === undefined) {
			return false;
		}

        // don't treat items that you got wrong once or twice as leeches immediately
        var sufficientErrors = ((item.review_statistics.meaning_incorrect + item.review_statistics.reading_incorrect) >= leechMinWrong);
        if (!sufficientErrors) {
            return false;
        }

        let reviewStats = item.review_statistics;
		let meaningScore = getLeechScore(reviewStats.meaning_incorrect, reviewStats.meaning_current_streak);
		let readingScore = getLeechScore(reviewStats.reading_incorrect, reviewStats.reading_current_streak);

		return meaningScore >= threshold || readingScore >= threshold;
	}

	function getLeechScore(incorrect, currentStreak) {
		return incorrect / Math.pow((currentStreak || 0.5), 1.5);
	}

    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    //---------------------------------------------------------INTEGRATION WITH EXTRA STUDY MOVER----------------------------------------------------------//
    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    // detects if the Extra Study Mover script is present. It should execute BEFORE the cockpit
    function isESMPresent() {
        return $('.es-mover').length;
    }

    // get the configured location from the Extra Study Mover added class
    // note: this also retrieves the applied style, but we can ignore that later
    function getESMLocation() {
        if (isESMPresent()) {
            var classList = $('.wk-panel--extra-study')[0].classList;
            return Array.from([... classList]).filter(name => name.startsWith('es-mover-')).map(name => name.slice(9));
        }

        return [];
    }

    // true if Extra Study Mover is present and it isn't configured for header or none
    // false if it is configured for header or none
    // AND false if Extra Study Mover does not exist
    function moveToESMLocation(esmLocations) {
        var useLocations = esmLocations;
        if (!useLocations) useLocations = getESMLocation();
        return !(useLocations.filter(name => (name == "header"))).length;
    }

    // true if the cockpit should move the Extra Study Panel at all
    function moveExtraStudy() {
        return !isESMPresent() || moveToESMLocation();
    }

    // translates both the ESM location (if present) and the cockpit setting (if needed)
    // to an equivalent cockpit setting
    function getExtraStudyLocationSetting() {
        if (!isESMPresent()) {
            return wkof.settings.db_cockpit.showExtraStudy;
        }
        else {
            var esmLocations = getESMLocation();
            if (moveToESMLocation(esmLocations)) {
                for (var loc in esmLocations) {
                    if (esmLocations[loc] == 'top')      return '7';  // cockpit doesn't have a proper top position available, so center top will have to do
                    if (esmLocations[loc] == 'above-rm') return '8';
                    if (esmLocations[loc] == 'above-lp') return '1';
                    if (esmLocations[loc] == 'below-lp') return '6';
                    if (esmLocations[loc] == 'above-ib') return '5';
                    if (esmLocations[loc] == 'below-ib') return '2';
                    if (esmLocations[loc] == 'above-rf') return '3';
                    if (esmLocations[loc] == 'below-rf') return '4';
                    if (esmLocations[loc] == 'none')     return '0';
                    // else zip, this might be a style, so go for the next one
                }
            }
            else {
                // not allowed to move (header)
                return '-1';
            }
        }

        // we only get here if ESM adds an extra location that we
        // don't recognise. Default to Above Level Progress
        console.log("Unknown Extra Study Panel location. Defaulting to Above Level Progress");
        return '1';
    }

    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    //---------------------------------------------------------INTEGRATION WITH REVIEW SUMMARY-------------------------------------------------------------//
    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    // detects if the Review Summary script is present. It should execute BEFORE the cockpit
    function isRSPresent() {
        return $('#reviewSummaryDiv').length;
    }
    function isRSVisible() {
        return isRSPresent() && wkof.settings.db_cockpit.showReviewSummary;
    }

    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    //---------------------------------------------------------------COMBINATION VISIBILITY----------------------------------------------------------------//
    //-----------------------------------------------------------------------------------------------------------------------------------------------------//

    function hasFutureLessons() {
        var lessonCount = parseInt($('.navigation-shortcut--lessons')[0].getAttribute("data-count"));
        return ((lockedCount.total + lessonCount) > 0);
    }

    function showLessonsButton() {
        return (   wkof.settings.db_cockpit.showMainButtons
                && (   !wkof.settings.db_cockpit.lvl60Lessons
                    || hasFutureLessons()
                   )
               );
    }

    function showLockedCount(optionalSpecificType) {
        var mustBeVisible = (wkof.settings.db_cockpit.showSrsLocked != '0');
        if (optionalSpecificType != undefined) {
            mustBeVisible = (wkof.settings.db_cockpit.showSrsLocked == optionalSpecificType);
        }
        return (   mustBeVisible
                && (   !wkof.settings.db_cockpit.lvl60Lessons
                    || hasFutureLessons()
                   )
               );
    }

    function showLevelProgress(allPassed) {
        return (   !allPassed
                || !wkof.settings.db_cockpit.lvl60Progress
               );
    }

    // true if either the forecast is visible or the extra study panel is displayed above/below the forecast position
    // included here as the latter is now possibly dependent on the ESM setting
    function isRightGridVisible(allPassed) {
        // need to show review forecast
        if (wkof.settings.db_cockpit.showForecast) {
            return true;
        }

        // need to show extra study
        var esLocation = getExtraStudyLocationSetting();
        if (   esLocation == '3'
            || esLocation == '4'
           ) {
            return true;
        }

        // need to show review button
        if (   wkof.settings.db_cockpit.showMainButtons
            && !isLeftGridVisible(allPassed)
           ) {
            return true;
        }

        if (   isRSVisible()
            && !isLeftGridVisible(allPassed)
            ) {
            return true;
        }

        return false;
    }

    function isLeftGridVisible(allPassed) {
        if (
            // need to show level progress panel
               showLevelProgress(allPassed)
            // need to show lessons button
            || showLessonsButton()
            // need to show locked count
            || showLockedCount()
           ) {
            return true;
        }

        // need to show extra study
        var esLocation = getExtraStudyLocationSetting();
        if (   esLocation == '-1'    // invisible extra study is visible recent mistakes on the left!
            || esLocation == '1'
            || esLocation == '6'
           ) {
            return true;
        }

        return false;
    }

    // NOTE! this function does not check for moving the reviews button to the left grid
    // always check isRightGridVisible() first
    function isTopRightCellVisible() {
        return (
            // need to show review button
            wkof.settings.db_cockpit.showMainButtons
            // or the review summary
            || isRSVisible()
           );
    }

    // NOTE this function only makes sense if leftGrid is known to be visible
    // it will block on a moved reviews button
    function isTopLeftCellVisible() {
        return (
            // need to show lessons button
               showLessonsButton()
            // need to show locked count
            || showLockedCount()
            // need to show reviews button or the summary
            || (   (   wkof.settings.db_cockpit.showMainButtons
                    || isRSVisible()
                   )
                && !isRightGridVisible(false)
               )
           );
    }

    function getLeftColumnWidth(allPassed) {
        var minWidth = 0;
        var marginToApply = 0;
        if (showLockedCount()) {
            minWidth += 120;
            marginToApply = 5;
        }
        if (showLessonsButton()) {
            minWidth += marginToApply;
            minWidth += 200;
            marginToApply = 10;
        }
        if (   !isRightGridVisible(allPassed)
            && wkof.settings.db_cockpit.showMainButtons
           ) {
            minWidth += marginToApply;
            minWidth += 200;
            marginToApply = 10;
            if (isRSVisible()) {
                minWidth += marginToApply;
                minWidth += 120;
                marginToApply = 10;
            }
        }

        if (minWidth < BASE_COLUMN_WIDTH) {
            var esLocation = getExtraStudyLocationSetting();
            if (   esLocation == '-1'   // invisible extra study is visible recent mistakes on the left!
                || esLocation == '1'
                || esLocation == '6'
                || showLevelProgress(allPassed)
               ) {
                minWidth = BASE_COLUMN_WIDTH;
            }
        }

        return minWidth;
    }

    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    //-----------------------------------------------------------------UTILITY FUNCTIONS-------------------------------------------------------------------//
    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    function containsSection(element, component) {
        return (element.querySelector(component) != null);
    }

    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    //--------------------------------------------------------------------PRESENTATION---------------------------------------------------------------------//
    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    var CANVAS_SIZE = [ 481, 401, 321 ]; // odd numbers to give it a center pixel
    function getCanvasSize() {
        var configured = wkof.settings.db_cockpit.srsSize ? parseInt(wkof.settings.db_cockpit.srsSize) : 0;
        return CANVAS_SIZE[configured];
    }
    function getCanvasGridWidth() {
        return (getCanvasSize() - 11) / 2;
    }
    var BASE_COLUMN_WIDTH = 335;

    // size definitions we'll populate after loading the settings
    var MAINBAR_WIDTH;
    var HELPERBAR_WIDTH;
    var canvasCenter = {
        "x": 0,
        "y": 0
    };

    // main srs circle
    var srsOutsideRadius;
    var srsInsideRadius;
    var srsTextRadius;
    // srs detail circle
    var srsOutsideDetailRadius;
    var srsInsideDetailRadius;
    var srsDetailTextRadius;
    // srs level circle
    var srsOutsideLevelRadius;
    var srsInsideLevelRadius;
    var srsLevelTextRadius;
    var levelProgressTextRadius;
    // additionalInfo
    var addInfo1TextRadius;

    function loadSizes() {
        MAINBAR_WIDTH = getCanvasSize() / 7;
        HELPERBAR_WIDTH = MAINBAR_WIDTH / 2;

        // pre calculations
        canvasCenter.x = (getCanvasSize() / 2);
        canvasCenter.y = (getCanvasSize() / 2);

        // main srs circle
        srsOutsideRadius = (getCanvasSize() - 1) / 2 - HELPERBAR_WIDTH;
        srsInsideRadius = srsOutsideRadius - MAINBAR_WIDTH;
        srsTextRadius = (srsOutsideRadius + srsInsideRadius) / 2;

        // srs detail circle
        srsOutsideDetailRadius = srsOutsideRadius + HELPERBAR_WIDTH;
        srsInsideDetailRadius = srsOutsideRadius;
        srsDetailTextRadius = (srsOutsideDetailRadius + srsInsideDetailRadius) / 2;

        // srs level circle
        srsOutsideLevelRadius = srsInsideRadius;
        srsInsideLevelRadius = srsInsideRadius - HELPERBAR_WIDTH;
        srsLevelTextRadius = (srsOutsideLevelRadius + srsInsideLevelRadius) / 2;
        levelProgressTextRadius = srsInsideRadius - (7 * HELPERBAR_WIDTH / 12);

        // additionalInfo
        addInfo1TextRadius = srsOutsideRadius - (HELPERBAR_WIDTH / 2);

        // and all the font sizes!!!
        var configured = wkof.settings.db_cockpit.srsSize ? parseInt(wkof.settings.db_cockpit.srsSize) : 0;
        var levelFontSize = 80 - 10*configured;
        var srsFontSize = 24 - 3*configured;
        var helperFontSize = 14 - 1.5*configured;
        var leechFontSize = 12 - configured;
        var levelProgressFontSize = 12 - configured;

        levelFont.fontStyle= "bold " + levelFontSize + "px Arial, sans-serif";
        levelFont.fontSize= calculateFontSize(levelFont.fontStyle);
        srsFont.fontStyle="bold " + srsFontSize + "px Arial, sans-serif";
        srsFont.fontSize= calculateFontSize(srsFont.fontStyle);
        helperFont.fontStyle= helperFontSize + "px Arial, sans-serif";
        helperFont.fontSize= calculateFontSize(helperFont.fontStyle);
        leechFont.fontStyle= leechFontSize + "px Arial, sans-serif";
        leechFont.fontSize= calculateFontSize(leechFont.fontStyle);
        levelProgressFont.fontStyle= "bold " + levelProgressFontSize + "px Arial, sans-serif";
        levelProgressFont.fontSize= calculateFontSize(levelProgressFont.fontStyle);
    }

    var srsStartAngle = [ 3.5, 2 ];
    var arcWidth = Math.PI * 2 / 6;

    var srsFont = {
        color: "#ffffff",
        fontStyle: "bold 24px Arial, sans-serif",
        fontSize: calculateFontSize("bold 24px Arial, sans-serif")
    }
    var levelFont = {
        color: function() {
            if (wkof.settings.db_cockpit.presetLevelColor == '0') {
                return {
                    start: "#dd0093",
                    end: "#0093dd"
                }
            }

            return {
                start: wkof.settings.db_cockpit.levelNrInside,
                end: wkof.settings.db_cockpit.levelNrOutside
            }
        },
        colorOld: {
            start: "#dd0093",
            end: "#0093dd"
        },
        fontStyle: "bold 80px Arial, sans-serif",
        fontSize: calculateFontSize("bold 80px Arial, sans-serif"),
        opacity: function() {
            if (wkof.settings.db_cockpit.presetLevelColor == '2') {
                return 0.6;
            }

            return 0.2;
        },
        lineWidth: 3
    }

    // srs detail circle
    var helperFont = {
        color: "#ffffff",
        fontStyle: "14px Arial, sans-serif",
        fontSize: calculateFontSize("14px Arial, sans-serif")
    }
    var leechFont = {
        color: "#ffffff",
        fontStyle: "12px Arial, sans-serif",
        fontSize: calculateFontSize("12px Arial, sans-serif"),
        bgColor: "#000000",
        opacity: function() {
            var leechDisplay = parseInt(wkof.settings.db_cockpit.leechDisplay);
            switch (leechDisplay) {
                case 0:
                    return 0.4;
                case 1:
                    return 0.4;
                case 2:
                    return 1.0;
                case 3:
                    return 0.6;
            }
        },
        highlightOpacity: 1.0,
        bgOpacity: 0.3,
        rounded: function() {
            return (wkof.settings.db_cockpit.leechDisplay == '3');
        },
        colorScheme: function() {
            if (wkof.settings.db_cockpit.leechDisplay == '3') {
                return {
                    color: "#000000",
                    bgColor: "#ffffff"
                }
            }
            else {
                return null;
            }
        }
    }
    var appOpacity = 0.7;
    var gurOpacity = 0.9;
    var opacityFactor = 0.85;

    // srs level circle
    var srsLevelColors = {
        bgColor: "#434343",
        darkBgColor: "#232629"
    }
    var levelProgressFont = {
        color: "#9df",
        fontStyle: "bold 12px Arial, sans-serif",
        fontSize: calculateFontSize("bold 12px Arial, sans-serif")
    };
    var dashLength = 8;
    var dashFont = {
        color: "#ddff99",
        lineWidth: 2
    }

    // additionalInfo
    var srsDrawing = [
        {
            srsAbbrev: "App",
            srsName: "Apprentice",
            srsLevel: 1,
            srsLogo: "apprentice",
            color: function() {
                return {
                    start: "#f0a",
                    end: "#dd0093"
                }
            }
        },
        {
            srsAbbrev: "Gur",
            srsName: "Guru",
            srsLevel: 5,
            srsLogo: "guru",
            color: function() {
                return {
                    start: "#aa38c6",
                    end: "#882d9e"
                }
            }
        },
        {
            srsAbbrev: "Mas",
            srsName: "Master",
            srsLevel: 7,
            srsLogo: "master",
            color: function() {
                return {
                    start: "#5571e2",
                    end: "#294ddb"
                }
            }
        },
        {
            srsAbbrev: "Enl",
            srsName: "Enlightened",
            srsLevel: 8,
            srsLogo: "enlightened",
            color: function() {
                return {
                    start: "#0af",
                    end: "#0093dd"
                }
            }
        },
        {
            srsAbbrev: "Bur",
            srsName: "Burned",
            srsLevel: 9,
            srsLogo: "burned",
            color:  function() {
                if (wkof.settings.db_cockpit.goldenBurn) {
                    return {
                        start: "#fbc550",
                        end: "#faac05",
                        progressText: "#777"
                    }
                }
                else {
                    return {
                        start: "#555",
                        end: "#434343"
                    }
                }
            }
        }
    ];

    var srsDetailSrs = [ [
        {
            srsAbbrev: "Appr1",
            color: "#dd0093",
            opacity: appOpacity * opacityFactor * opacityFactor * opacityFactor
        },
        {
            srsAbbrev: "Appr2",
            color: "#dd0093",
            opacity: appOpacity * opacityFactor * opacityFactor
        },
        {
            srsAbbrev: "Appr3",
            color: "#dd0093",
            opacity: appOpacity * opacityFactor
        },
        {
            srsAbbrev: "Appr4",
            color: "#dd0093",
            opacity: appOpacity
        }
    ],
    [
        {
            srsAbbrev: "Guru1",
            color: "#882d9e",
            opacity: gurOpacity * opacityFactor
        },
        {
            srsAbbrev: "Guru2",
            color: "#882d9e",
            opacity: gurOpacity
        }
    ] ];

    // create the main panels
    function createPanels() {
       var cockpitPanel = document.createElement("div");
       cockpitPanel.id="cockpitPanel";

       var dbc_gas = document.createElement("div");
       dbc_gas.id="dbc_gas";
       cockpitPanel.append(dbc_gas);

       var dbc_currentLevel = document.createElement("div");
       dbc_currentLevel.id="dbc_currentLevel";
       cockpitPanel.append(dbc_currentLevel);

       var dbc_core = document.createElement("div");
       dbc_core.id="dbc_core";
       cockpitPanel.append(dbc_core);

       var dbc_bar60 = document.createElement("div");
       dbc_bar60.id="dbc_bar60";
       cockpitPanel.append(dbc_bar60);

       var dbc_cruise = document.createElement("div");
       dbc_cruise.id="dbc_cruise";
       cockpitPanel.append(dbc_cruise);

       var dbc_forecast = document.createElement("div");
       dbc_forecast.id="dbc_forecast";
       cockpitPanel.append(dbc_forecast);

       if ($('#timeline').length) {
           $('#timeline').after(cockpitPanel);
       }
       else {
           $('.dashboard__content').before(cockpitPanel);
       }
    }

    // that turbo frame is obnoxious
    function restyleLessonsButton() {
        // button section update addition styling
        $('.todays-lessons').css("padding", "var(--spacing-tight)");

        $('.todays-lessons__content').css('display', 'inline');
        $('.todays-lessons__content').css("position", "relative");
        $('.todays-lessons__text-content').css("position", "absolute");
        $('.todays-lessons__text-content').css("bottom", "0px");
        $('.todays-lessons__text-content').css("right", "0px");

        $('.todays-lessons__text').detach();
        $('.todays-lessons__subtitle').detach();
        $('.todays-lessons__title-text').detach();

        $('.todays-lessons__image-content').css("margin-bottom", "0px");

        var buttons = $('.todays-lessons__button');
        for (var i = 0; i < buttons.length; i++) {
            buttons[i].style["width"] = "100%";
            var buttonInternal = buttons[i].querySelector('.wk-button');
            buttonInternal.style["padding"] = "var(--spacing-tight)";
        }
    }

    // due to the way the lessons section loads, this must be done later
    // and since we can't tell which is the lesson and which is the review button until AFTER
    // the lesson turboframe loads, we delay both of 'm
    function delayedButtonStyling() {
        var lessonsButton = null;
        var reviewsButton = null;
        var buttonSections = document.querySelectorAll('.dashboard__lessons-and-reviews-section');
        buttonSections.forEach(section => {
            if (containsSection(section, '.todays-lessons')) {
                lessonsButton = section.parentNode.removeChild(section);
            }
            if (containsSection(section, '.reviews-dashboard')) {
                reviewsButton = section.parentNode.removeChild(section);
            }
        });

        $('#dbc_gas').append(lessonsButton);
        $('#dbc_gas .dashboard__lessons-and-reviews-section').css("width", "168px");
        $('#dbc_gas .dashboard__lessons-and-reviews-section').css("float", "right");

        // delay further
        setTimeout(restyleLessonsButton, 1000);
        // and twice for safety
        setTimeout(restyleLessonsButton, 2000);

        var reviewButtonBlock = document.createElement("div");
        reviewButtonBlock.id="reviewButtonBlock";
        $('#dbc_cruise').append(reviewButtonBlock);
        $('#reviewButtonBlock').append(reviewsButton);
        reviewsButton.style["width"] = "168px";

        // button section update addition styling
        $('.reviews-dashboard').css("padding", "var(--spacing-tight)");

        $('.reviews-dashboard__content').css('display', 'inline');
        $('.reviews-dashboard__content').css("position", "relative");
        $('.reviews-dashboard__text-content').css("position", "absolute");
        $('.reviews-dashboard__text-content').css("bottom", "0px");
        $('.reviews-dashboard__text-content').css("right", "0px");

        $('.reviews-dashboard__text').detach();
        $('.reviews-dashboard__title-text').detach();

        $('.reviews-dashboard__image-content').css("margin-bottom", "0px");

        var buttons = $('.reviews-dashboard__button');
        for (var i = 0; i < buttons.length; i++) {
            buttons[i].style["width"] = "100%";
            var buttonInternal = buttons[i].querySelector('.wk-button');
            buttonInternal.style["padding"] = "var(--spacing-tight)";
        }

        // integration with Kumirei's Review Hover Details
        var reviewHoverDetails = $('.dashboard #review_hover_details');
        if (reviewHoverDetails) {
            reviewHoverDetails.detach();
            $('.lessons-and-reviews__reviews-button').after(reviewHoverDetails);
        }
        if (isRSPresent()) {
            var summaryDiv = $('#reviewSummaryDiv').detach();
            $('#dbc_cruise').append(summaryDiv);
            $('#reviewSummaryTile').css("float", "right");
            $('#reviewSummaryTile').css("width", "120px");
            $('#reviewSummaryTile').css("height", "100px");
            $('#reviewSummaryTile #rs_tile_span_header').css("font-size", "14px");
            $('#reviewSummaryTile #rs_tile_span_header').css("top", "6px");
            $('#reviewSummaryTile #rs_tile_span_percentage').css("font-size", "28px");
            $('#reviewSummaryTile #rs_tile_span_percentage').css("top", "16px");
            $('#reviewSummaryTile #rs_tile_icon_percentage').css("top", "-2px");
        }
    }

    // create the panel contents
    function createPanelContents() {
        var speedometer = document.createElement("canvas");
        speedometer.id="speedometer";
        $('#dbc_core').append(speedometer);
        var canvasTooltips = document.createElement("div");
        canvasTooltips.id="canvasTooltips";
        canvasTooltips.className="db_cockpit_tooltips";
        $('#dbc_core').append(canvasTooltips);
        var canvasClicktips = document.createElement("div");
        canvasClicktips.id="canvasClicktips";
        canvasClicktips.className="db_cockpit_tooltips";
        $('#dbc_core').append(canvasClicktips);

        // temporarily relocate the button sections until we can figure out which is which
        var buttonSections = document.querySelectorAll('.dashboard__lessons-and-reviews-section');
        buttonSections.forEach(section => {
            $('#dbc_cruise').append(section.parentNode.removeChild(section));
        });

        // lessons button is now a turbo frame and can load late
        setTimeout(delayedButtonStyling, 1000);

        var extraRecentStudy = document.createElement("div");
        extraRecentStudy.id="extraRecentStudy";
        $('#dbc_currentLevel').append(extraRecentStudy);
        var recentMistakes = $('.wk-panel--recent-mistakes');
        recentMistakes.detach();
        recentMistakes.css("margin-bottom", "10px");
        $('#extraRecentStudy').append(recentMistakes);
        if (moveExtraStudy()) {
            var extraStudy = $('.wk-panel--extra-study');
            extraStudy.detach();
            $('#extraRecentStudy').append(extraStudy);
        }
        var currentLevelProgress = $('.wk-panel--level-progress').detach();
        $('#dbc_currentLevel').append(currentLevelProgress);

        var forecast = $('.wk-panel--review-forecast').detach();
        $('#dbc_forecast').append(forecast);
        forecast.css("margin-bottom", "10px");

        // clean up items that are no longer needed
        // new dashboard update, all are individual sections now. I can't just hide these, because they occupy individual
        // cells in the dashboard content grid. Removing them allows me to remove the grid rows
        $('.dashboard__lessons-and-reviews').detach();
        $('.dashboard__recent-mistakes').detach();
        $('.dashboard__extra-study').detach();
        $('.dashboard__level-progress').detach();
        $('.dashboard__review-forecast').detach();
        // and move the rest up
        $('.dashboard__srs-progress').css("grid-row", "1/2");
        $('.dashboard__item-lists').css("grid-row", "2/3");
        $('.dashboard__community-banner').css("grid-row", "3/4");
        // tricky tricky. New dashboard update puts a section around srs-progress (dashboard__srs-progress), which I ideally
        // want to hide, but other scripts that place around the srs-progress would be hidden as well, so we keep it at this for now
        $('.srs-progress').addClass('hidden');

        $("#speedometer").mouseenter(function(e) { reOffsetCanvas(e); });
        $("#speedometer").mousemove(function(e) { handleCanvasMouseMove(e); });
        $("#speedometer").mouseleave(function(e) {
            // hide all tooltips
            $('.db_cockpit_tooltipText_visible').removeClass('db_cockpit_tooltipText_visible');
        });
        $("#speedometer").click(function(e) { handleCanvasMouseClick(e); });
        $("body").click(hideClickTips);
    }

    function repositionMainPanel() {
        // ultimate timeline does not have a margin at the bottom, so we insert margin top if needed
        if ($('#timeline').length) {
            $("#cockpitPanel").css("margin-top", "10px");
        }
        else {
            $("#cockpitPanel").css("margin-top", "0px");
        }
    }

    // empty the canvas for redrawing
    function clearCanvas() {
        var speedometer = $('#speedometer')[0];
        speedometer.width = getCanvasSize();
        speedometer.height = getCanvasSize();

        var ctx = speedometer.getContext("2d");
        ctx.clearRect(0, 0, getCanvasSize(), getCanvasSize());
        hotspots = [];
        clickHotspots = [];
        highlightHotspots = [];
        currentHighlight = null;
        $('#canvasTooltips div').detach();
        $('#canvasTooltips span').detach();
        $('#canvasClicktips div').detach();
    }

    // creates a linear gradient for an arc shape (between opposing corners)
    function createArcGradient(ctx, centerX, centerY, outsideRadius, insideRadius, startAngle, endAngle, color) {
        if (color.start != undefined) {
            var ctxGradient = ctx.createLinearGradient(
                Math.cos(endAngle) * outsideRadius + centerX,
                Math.sin(endAngle) * outsideRadius + centerY,
                Math.cos(startAngle) * insideRadius + centerX,
                Math.sin(startAngle) * insideRadius + centerY
                );

            ctxGradient.addColorStop(0, color.start);
            ctxGradient.addColorStop(1, color.end);

            return ctxGradient;
        }
        else {
             return color;
        }
    }

    // draws and fill the arc sections
    function drawSRSArc(ctx, centerX, centerY, outsideRadius, insideRadius, startAngle, endAngle, color, opacity) {

        ctx.save();

        ctx.fillStyle = createArcGradient(ctx, centerX, centerY, outsideRadius, insideRadius, startAngle, endAngle, color);
        if (opacity != undefined) {
            ctx.globalAlpha=opacity;
        }

	    ctx.beginPath();
	    ctx.moveTo(centerX,centerY);
	    ctx.arc(centerX, centerY, outsideRadius, startAngle, endAngle);
        ctx.arc(centerX, centerY, insideRadius, endAngle, startAngle, true);
	    ctx.closePath();

	    ctx.fill();

        ctx.restore();
    }

    function calculateFontSize(font) {
        var fontsize = font.match(/[\d|\.]+\s?px/g);
        if (fontsize != undefined) {
            fontsize = fontsize[0].match(/[\d|\.]+/g);
            if (fontsize != undefined) {
                fontsize = fontsize[0];
            }
        }
        else {
            fontsize = font.match(/[\d|\.]+\s?em/g);
            if (fontsize.length != undefined) {
                fontsize = fontsize[0].match(/[\d|\.]+/g);
                if (fontsize.length != undefined) {
                    // estimate is good enough, we're only shifting by 1/4 of these pixels
                    fontsize = fontsize[0] * 16;
                }
            }
        }
        if (fontsize == undefined) {
            fontsize = 16;
        }

        return fontsize;
    }

    // adds text to the canvas
    function drawCenteredText(ctx, textX, textY, text, textFont, bright) {

        ctx.save();

        ctx.font = textFont.fontStyle;
        if (textFont.lineWidth != undefined) {
            ctx.lineWidth = textFont.lineWidth;
        }

        var metrics = ctx.measureText(text);
        var placeX = textX - metrics.width / 2;
        var fontsize = textFont.fontSize;
        var placeY = textY + fontsize / 4;

        var resultDimensions = {
            minX: placeX - 2,
            minY: placeY - parseInt(fontsize) + 1,
            width: metrics.width + 4,
            height: parseInt(fontsize) + 2
        };

        // background opacity
        if (textFont.bgOpacity != undefined) {
            ctx.globalAlpha = textFont.bgOpacity;
        }
        // draw background for the text if required
        if (textFont.bgColor != undefined) {
            ctx.fillStyle = prioritiseColorSchemeBgColor(textFont);
            if (textFont.rounded()) {
                ctx.beginPath();
                ctx.ellipse(resultDimensions.minX + resultDimensions.width/2, resultDimensions.minY + resultDimensions.height/2, resultDimensions.width * 0.6, resultDimensions.height * 0.6, 0, 0, 2 * Math.PI);
                ctx.fill();
            }
            else {
                ctx.fillRect(resultDimensions.minX, resultDimensions.minY, resultDimensions.width, resultDimensions.height);
            }
        }

        // foreground opacity
        if (textFont.opacity != undefined) {
            if ((bright != undefined) && bright && (textFont.highlightOpacity != undefined)) {
                ctx.globalAlpha = textFont.highlightOpacity;
            }
            else {
                ctx.globalAlpha = textFont.opacity();
            }
        }

        var resolvedColor = textFont.color;
        if (typeof resolvedColor == 'function') {
            resolvedColor = textFont.color();
        }
        if (resolvedColor.start != undefined) {
            var ctxGradient = ctx.createRadialGradient(
                textX,
                textY,
                1,
                textX,
                textY,
                fontsize
            );

            ctxGradient.addColorStop(0, resolvedColor.start);
            ctxGradient.addColorStop(1, resolvedColor.end);

            ctx.fillStyle = ctxGradient;
            ctx.strokeStyle = ctxGradient;
        }
        else {
            ctx.strokeStyle = prioritiseColorSchemeColor(textFont);
            ctx.fillStyle = prioritiseColorSchemeColor(textFont);
        }

        if (textFont.lineWidth != undefined) {
            ctx.strokeText(text, placeX, placeY);
        }
        else {
            ctx.fillText(text, placeX, placeY);
        }

        ctx.restore();

        return resultDimensions;
    }

    function prioritiseColorSchemeColor(font) {
        if (font.colorScheme && font.colorScheme() && font.colorScheme().color) {
            return font.colorScheme().color;
        }
        return font.color;
    }
    function prioritiseColorSchemeBgColor(font) {
        if (font.colorScheme && font.colorScheme() && font.colorScheme().bgColor) {
            return font.colorScheme().bgColor;
        }
        return font.bgColor;
    }

    // adds arrow heads to the canvas
    function drawArrowHead(ctx, radius, angle, color, fontSize) {

        ctx.save();

        // we're going to rotate around the center point
        ctx.translate(canvasCenter.x, canvasCenter.y);
        ctx.rotate(angle);

        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.moveTo(radius, 0);
        ctx.lineTo(radius-fontSize, 0.5*fontSize);
        ctx.lineTo(radius-(2*fontSize/3), 0);
        ctx.lineTo(radius-fontSize, -0.5*fontSize);
        ctx.closePath();

        ctx.fill();

        ctx.restore();
    }

    // adds a line to the canvas
    function drawLine(ctx, startPoint, endPoint, lineFont) {

        ctx.save();

        if (lineFont.lineWidth != undefined) {
            ctx.lineWidth = lineFont.lineWidth;
        }
        ctx.strokeStyle = lineFont.color;

        ctx.beginPath();
        ctx.moveTo(startPoint.x, startPoint.y);
        ctx.lineTo(endPoint.x, endPoint.y);
        ctx.stroke();

        ctx.restore();
    }

    // keep track of the canvas position on scrolling/resizing
    function reOffsetCanvas(){
        if ($('#speedometer').length) {
            var BB = $('#speedometer')[0].getBoundingClientRect();
            offsetX = BB.left;
            offsetY = BB.top;
            windowOffsetX = $(window).scrollLeft();
            windowOffsetY = $(window).scrollTop();
        }
    }
    var offsetX, offsetY;
    var windowOffsetX, windowOffsetY;
    window.onscroll = function(e) { reOffsetCanvas(); }
    window.onresize = function(e) { reOffsetCanvas(); }

    function handleCanvasMouseMove(e){
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();

        var mouseX = parseInt(e.clientX - offsetX);
        var mouseY = parseInt(e.clientY - offsetY);

        for(var i = 0; i < hotspots.length; i++) {
            var h = hotspots[i];
            var dx = mouseX - h.x;
            var dy = mouseY - h.y;
            if(dx * dx + dy * dy < h.radius * h.radius) {
                showTooltip(hotspots[i]);
                highlightSpot(currentHighlight, false);
                return;
            }
        }

        // if no hotspot matches, hide all tooltips
        var visibleTT = $('.db_cockpit_tooltipText_visible');
        visibleTT.removeClass('db_cockpit_tooltipText_visible');

        // highlighting only for option 1
        if (wkof.settings.db_cockpit.leechDisplay != '1') { return; }

        // then check for highlights
        for (i = 0; i < highlightHotspots.length; i++) {
            h = highlightHotspots[i];
            if (   (mouseX > h.textBg.minX) && (mouseX < (h.textBg.minX + h.textBg.width))
                && (mouseY > h.textBg.minY) && (mouseY < (h.textBg.minY + h.textBg.height))
               ) {
                highlightSpot(h, true);
                return;
            }
        }

        // if no highlights, then remove any current
        highlightSpot(currentHighlight, false);
    }

    function showTooltip(hotspot) {
        // hide all other tooltips
        var visibleTT = $('.db_cockpit_tooltipText_visible:not(#' + hotspot.id + ')');
        visibleTT.removeClass('db_cockpit_tooltipText_visible');

        // expose current tooltip if it wasn't already
        if (!$('.db_cockpit_tooltipText_visible #' + hotspot.id).length) {
            var spanWidth = $('#' + hotspot.id).width();
            var spanHeight = $('#' + hotspot.id).height();
            var offsetLeft = Math.floor(hotspot.x + offsetX + windowOffsetX - spanWidth * 0.4);
            var offsetTop = Math.floor(hotspot.y + offsetY + windowOffsetY - spanHeight - 30);

            $('#' + hotspot.id).css('left', offsetLeft + 'px');
            $('#' + hotspot.id).css('top', offsetTop + 'px');
            $('#' + hotspot.id).addClass('db_cockpit_tooltipText_visible');
        }
    }

    var hotspots = [];
    function createHotspot(spotX, spotY, spotRadius, spotText) {
        var spotId = "tt" + Math.floor(spotX) + "_" + Math.floor(spotY);
        hotspots.push( { x: spotX, y: spotY, radius: spotRadius, id: spotId } );
        $('#canvasTooltips').append(
            '<span class="db_cockpit_tooltipText" id="' + spotId + '">' + spotText + '</span>'
        );
    }
    function createBreakDownHotspot(spotX, spotY, spotRadius, logo, rTotal, kTotal, vTotal) {
        var spotId = "tt" + Math.floor(spotX) + "_" + Math.floor(spotY);
        hotspots.push( { x: spotX, y: spotY, radius: spotRadius, id: spotId } );
        $('#canvasTooltips').append(
            '<div class="db_cockpit_tooltipBreakdownText" id="' + spotId + '">'
          +   '<div class="srs-logo ' + logo + '"></div>'
          +   '<div class="tt_bd_totals"><ul>'
          +     '<li>rad:<span>' + rTotal + '</span></li>'
          +     '<li>kan:<span>' + kTotal + '</span></li>'
          +     '<li>voc:<span>' + vTotal + '</span></li>'
          +   '</ul></div>'
          + '</div>'
        );
    }

    function handleCanvasMouseClick(e){
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();

        var mouseX = parseInt(e.clientX - offsetX);
        var mouseY = parseInt(e.clientY - offsetY);

        for(var i = 0; i < clickHotspots.length; i++) {
            var h = clickHotspots[i];
            var dx = mouseX - h.x;
            var dy = mouseY - h.y;
            if(dx * dx + dy * dy < h.radius * h.radius) {
                showClicktip(clickHotspots[i]);
                return;
            }
        }

        // if no clickHotspot matches, hide all clicktips
        var visibleTT = $('.db_cockpit_clicktipText_visible');
        visibleTT.removeClass('db_cockpit_clicktipText_visible');

        // temp addition to test level up animation
//        dx = mouseX - canvasCenter.x;
//        dy = mouseY - canvasCenter.y;
//        if (dx * dx + dy * dy < 6400) {
//            levelUpAnimation(11, 12);
//        }
    }

    function hideClickTips(e){
        // hide all click tips
        var visibleTT = $('.db_cockpit_clicktipText_visible');
        visibleTT.removeClass('db_cockpit_clicktipText_visible');
    }

    function showClicktip(hotspot) {
        // hide all other clicktips
        var visibleTT = $('.db_cockpit_clicktipText_visible:not(#' + hotspot.id + ')');
        visibleTT.removeClass('db_cockpit_clicktipText_visible');

        // expose current clicktip if it wasn't already
        if (!$('.db_cockpit_clicktipText_visible #' + hotspot.id).length) {
            var spanWidth = $('#' + hotspot.id).width();
            var spanHeight = $('#' + hotspot.id).height();
            var offsetLeft = Math.floor(hotspot.x + offsetX + windowOffsetX - spanWidth * 0.4);
            var offsetTop = Math.floor(hotspot.y + offsetY + windowOffsetY - spanHeight - 30);

            $('#' + hotspot.id).css('left', offsetLeft + 'px');
            $('#' + hotspot.id).css('top', offsetTop + 'px');
            $('#' + hotspot.id).addClass('db_cockpit_clicktipText_visible');
        }
    }

    function scrollDialLevel(e, currentSpotId, toLevel) {

        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();

        // get current spot
        var currentDialDiv = $('#' + currentSpotId);
        // get next dial div and position
        var nextDialDiv = $('#' + createDialDivId(toLevel));
        nextDialDiv.css('left', currentDialDiv.css('left'));
        nextDialDiv.css('top', currentDialDiv.css('top'));

        // swap visibility
        currentDialDiv.removeClass('db_cockpit_clicktipText_visible');
        nextDialDiv.addClass('db_cockpit_clicktipText_visible');
    }

    function hideZero(value) {
        if (value == 0) {
            return "&nbsp;";
        }
        else {
            return value;
        }
    }

    var clickHotspots = [];
    function addDialHotspot(spotX, spotY, spotRadius, levelData) {
        var spotId = createDialDivId(levelData.level);
        clickHotspots.push( { x: spotX, y: spotY, radius: spotRadius, id: spotId } );
    }

    function createDialDiv(levelData, dialDiv) {
        var spotId = createDialDivId(levelData.level);

        var dialDivElem =
            '<div class="db_cockpit_tooltipDialText" id="' + spotId + '">'
          +   '<table><tr>'
          +     '<td class="db_cockpit_ctd_summary" colspan="2" style="width:100px">level</td>'
          +     '<td style="width:20px">&nbsp;</td><td class="db_cockpit_ctd_header">passed</td>'
          +     '<td class="db_cockpit_ctd_header">rad</td><td class="db_cockpit_ctd_header">kan</td><td class="db_cockpit_ctd_header">voc</td>'
          +     '</tr><tr>'
          +     '<td class="db_cockpit_ctd_level db_cockpit_ctd_summary" colspan="2" rowspan="3">'
          +     '<span class="db_cockpit_ctd_link" id="db_cockpit_' + levelData.level + '_previousLink">&nbsp;</span> '
          +      levelData.level
          +      ' <span class="db_cockpit_ctd_link" id="db_cockpit_' + levelData.level + '_nextLink">&nbsp;</span></td>'
          +     '<td class="db_cockpit_ctd_category" style="width:20px">-</td><td>&nbsp;</td>'
          +     '<td>' + hideZero(levelData.unstarted.radical) + '</td><td>' + hideZero(levelData.unstarted.kanji) + '</td><td>' + hideZero(levelData.unstarted.vocabulary) + '</td>'
          +     '</tr><tr>'
          +     '<td class="db_cockpit_ctd_category" style="width:20px">A</td><td>' + levelData.App.passed + '%</td>'
          +     '<td>' + hideZero(levelData.App.radical) + '</td><td>' + hideZero(levelData.App.kanji) + '</td><td>' + hideZero(levelData.App.vocabulary) + '</td>'
          +     '</tr><tr>'
          +     '<td class="db_cockpit_ctd_category" style="width:20px">G</td><td>' + levelData.Gur.passed + '%</td>'
          +     '<td>' + hideZero(levelData.Gur.radical) + '</td><td>' + hideZero(levelData.Gur.kanji) + '</td><td>' + hideZero(levelData.Gur.vocabulary) + '</td>'
          +     '</tr><tr>'
          +     '<td class="db_cockpit_ctd_summary" colspan="2">progress</td>'
          +     '<td class="db_cockpit_ctd_category" style="width:20px">M</td><td>' + levelData.Mas.passed + '%</td>'
          +     '<td>' + hideZero(levelData.Mas.radical) + '</td><td>' + hideZero(levelData.Mas.kanji) + '</td><td>' + hideZero(levelData.Mas.vocabulary) + '</td>'
          +     '</tr><tr>'
          +     '<td class="db_cockpit_ctd_score db_cockpit_ctd_summary" colspan="2" rowspan="2">' + levelData.score + '%</td>'
          +     '<td class="db_cockpit_ctd_category" style="width:20px">E</td><td>' + levelData.Enl.passed + '%</td>'
          +     '<td>' + hideZero(levelData.Enl.radical) + '</td><td>' + hideZero(levelData.Enl.kanji) + '</td><td>' + hideZero(levelData.Enl.vocabulary) + '</td>'
          +     '</tr><tr>'
          +     '<td class="db_cockpit_ctd_category" style="width:20px">B</td><td>' + levelData.Bur.passed + '%</td>'
          +     '<td>' + hideZero(levelData.Bur.radical) + '</td><td>' + hideZero(levelData.Bur.kanji) + '</td><td>' + hideZero(levelData.Bur.vocabulary) + '</td>'
          +   '</tr></table>'
          + '</div>';

        $('#canvasClicktips').append(dialDivElem);

        if (dialDiv.previousLevel) {
            $('#db_cockpit_' + levelData.level + '_previousLink').click(function(e) { scrollDialLevel(e, spotId, dialDiv.previousLevel); });
            $('#db_cockpit_' + levelData.level + '_previousLink').addClass("db_cockpit_ctd_clickable");
            $('#db_cockpit_' + levelData.level + '_previousLink').html("&lt;");
        }
        if (dialDiv.nextLevel) {
            $('#db_cockpit_' + levelData.level + '_nextLink').click(function(e) { scrollDialLevel(e, spotId, dialDiv.nextLevel); });
            $('#db_cockpit_' + levelData.level + '_nextLink').addClass("db_cockpit_ctd_clickable");
            $('#db_cockpit_' + levelData.level + '_nextLink').html("&gt;");
        }
    }

    var highlightHotspots = [];
    var currentHighlight;
    function createHighlightHotspot(centerX, centerY, outsideRadius, insideRadius, startAngle, endAngle, color, opacity,
                                    minX, minY, width, height, textX, textY, text, font) {
        var spotId = "hl" + Math.floor(minX) + "_" + Math.floor(minY);
        highlightHotspots.push({ id: spotId,
                                 arc: { centerX: centerX, centerY: centerY, outsideRadius: outsideRadius, insideRadius: insideRadius,
                                        startAngle: startAngle, endAngle: endAngle, color: color, opacity: opacity },
                                 textBg: { minX: minX, minY: minY, width: width, height: height },
                                 text: { textX: textX, textY: textY, text: text, font: font }
                               });
    }

    function highlightSpot(highlightHotspot, bright) {
        if (bright && (currentHighlight != undefined)) {
            if (currentHighlight.id == highlightHotspot.id) {
                // done
                return;
            }
            highlightSpot(currentHighlight, false);
        }
        if (!bright && (currentHighlight == undefined)) {
            // also done
            return;
        }

        drawHighlightSpot(highlightHotspot, bright);
        if (bright) {
            currentHighlight = highlightHotspot;
        }
        else {
            currentHighlight = null;
        }
    }

    function drawHighlightSpot(spot, bright) {
        var ctx = $('#speedometer')[0].getContext("2d");

        ctx.save();
        // clear the area
        ctx.clearRect(spot.textBg.minX, spot.textBg.minY, spot.textBg.width, spot.textBg.height);
        // and redraw the background arc
        if (spot.arc.opacity != undefined) {
            ctx.globalAlpha = spot.arc.opacity;
        }
        ctx.fillStyle = createArcGradient(ctx, spot.arc.centerX, spot.arc.centerY, spot.arc.outsideRadius, spot.arc.insideRadius, spot.arc.startAngle, spot.arc.endAngle, spot.arc.color);
        ctx.fillRect(spot.textBg.minX, spot.textBg.minY, spot.textBg.width, spot.textBg.height);
        ctx.restore();

        // then redraw the text, highlighted or not
        drawCenteredText(ctx, spot.text.textX, spot.text.textY, spot.text.text, spot.text.font, bright);
    }

    // main srs progress blocks
    var levelUp = false;
    var oldLevel;
    function drawSRSProgress(itemBreakdown) {
        var ctx = $('#speedometer')[0].getContext("2d");
        var countsBySRS = itemBreakdown.countBySRS.bySRSArray;

        var totalCount = 0;
        var radicalCount = 0;
        var kanjiCount = 0;
        var vocabularyCount = 0;
        var sumFunction = function(countItem) {
            totalCount += countItem.total;
            radicalCount += countItem.radical;
            kanjiCount += countItem.kanji;
            vocabularyCount += countItem.vocabulary;
        };

        var barStartAngle = srsStartAngle[parseInt(wkof.settings.db_cockpit.srsOrientation)];
        for(var srsLvl in srsDrawing) {
            drawSRSArc(ctx, canvasCenter.x, canvasCenter.y, srsOutsideRadius, srsInsideRadius,
                       ((barStartAngle + parseInt(srsLvl))) * arcWidth, ((barStartAngle + parseInt(srsLvl) + 1)) * arcWidth, srsDrawing[srsLvl].color());

            var relatedCounts = filterBySRSCountOnAbbrev(countsBySRS, srsDrawing[srsLvl].srsAbbrev);
            totalCount = 0;
            radicalCount = 0;
            kanjiCount = 0;
            vocabularyCount = 0;
            relatedCounts.forEach(sumFunction);

            var textX = Math.cos((barStartAngle + parseInt(srsLvl) + 0.5) * arcWidth) * srsTextRadius + canvasCenter.x;
            var textY = Math.sin((barStartAngle + parseInt(srsLvl) + 0.5) * arcWidth) * srsTextRadius + canvasCenter.y;
            drawCenteredText(ctx, textX, textY, totalCount, srsFont);
            createBreakDownHotspot(textX, textY, srsFont.fontSize, srsDrawing[srsLvl].srsLogo, radicalCount, kanjiCount, vocabularyCount);
        }

        if (levelUp) {
            // previous level was drawn earlier, draw it again in its previous form and prepare level up animation
            drawCenteredText(ctx, canvasCenter.x, canvasCenter.y, oldLevel, levelFont);
            setTimeout("window.db_cockpit_levelUpAnimation(" + oldLevel + ", " + itemBreakdown.level + ")", 100);
            // don't recelebrate upon redraw. Note that we do 'celebrate' upon a reset as well - but the animation isn't too festive,
            // so it can be considered an update instead of a celebration.
            levelUp = false;
        }
        else {
            var newLevel = $('li.user-summary__attribute a')[0].href.split('/level/')[1];
            if (newLevel != itemBreakdown.level) {
                // we just leveled OR just reset and are drawing the old level from the cache
                levelUp = true;
                oldLevel = itemBreakdown.level;
            }
            // draw the user level in the center
            drawCenteredText(ctx, canvasCenter.x, canvasCenter.y, itemBreakdown.level, levelFont);
        }
    }

    var FRAME_DURATION = 80;
    function levelUpAnimation(oldLevel, newLevel) {
        var ctx = $('#speedometer')[0].getContext("2d");
        ctx.font = levelFont.fontStyle;
        ctx.lineWidth = levelFont.lineWidth;
        var metrics = ctx.measureText(oldLevel);
        var rectWidth = metrics.width;
        metrics = ctx.measureText(newLevel);
        if (metrics.width > rectWidth) {
            rectWidth = metrics.width;
        }
        var levelFontSize = levelFont.fontSize;
        var offsetTop = levelFontSize * 3 / 4;
        var clearRectangle = "{ fromX: " + ((canvasCenter.x - rectWidth / 2 - 1)) +
                             ", fromY: " + ((canvasCenter.y - offsetTop - 1)) +
                             ", width: " + ((rectWidth + 2)) +
                             ", height: " + ((parseInt(levelFontSize) + 5)) + " }";

        var frame = 1;
        for (var tempSize = levelFontSize; tempSize > 0; tempSize -= 10) {
            setTimeout("window.db_cockpit_levelUpAnimationFrame(" + clearRectangle + ", " + tempSize + ", " + oldLevel + ", " + frame + ")", frame * FRAME_DURATION);
            frame++;
        }

        for (tempSize = levelFontSize % 10; tempSize <= levelFontSize; tempSize += 10) {
            setTimeout("window.db_cockpit_levelUpAnimationFrame(" + clearRectangle + ", " + tempSize + ", " + newLevel + ", " + frame + ")", frame * FRAME_DURATION);
            frame++;
        }
    }
    window.db_cockpit_levelUpAnimation = levelUpAnimation;

    function levelUpAnimationFrame(rectangle, fontSize, text, frame) {
        var ctx = $('#speedometer')[0].getContext("2d");
        ctx.clearRect(rectangle.fromX, rectangle.fromY, rectangle.width, rectangle.height);
        var tempFont = { color: levelFont.color, opacity: levelFont.opacity, lineWidth: levelFont.lineWidth,
                         fontStyle: levelFont.fontStyle.replace(/[\d|\.]+\s?px/g, fontSize + "px"),
                         fontSize: fontSize }
        drawCenteredText(ctx, canvasCenter.x, canvasCenter.y, text, tempFont);
    }
    window.db_cockpit_levelUpAnimationFrame = levelUpAnimationFrame;

     // srs detail progress, dependent on setting
    function drawSRSProgressDetail(itemBreakdown) {
        var ctx = $('#speedometer')[0].getContext("2d");
        var countsBySRS = itemBreakdown.countBySRS.bySRSArray;

        if (wkof.settings.db_cockpit.showSrsDetail) {
            var barStartAngle = srsStartAngle[parseInt(wkof.settings.db_cockpit.srsOrientation)];
            for (var srsIndex in srsDetailSrs) {
                for(var srsLvl in srsDetailSrs[srsIndex]) {
                    drawSRSArc(ctx, canvasCenter.x, canvasCenter.y, srsOutsideDetailRadius , srsInsideDetailRadius,
                               (barStartAngle + parseInt(srsIndex) + parseInt(srsLvl) * 0.25 * (parseInt(srsIndex) + 1)) * arcWidth,
                               (barStartAngle + parseInt(srsIndex) + (parseInt(srsLvl) + 1) * 0.25 * (parseInt(srsIndex) + 1)) * arcWidth,
                               srsDetailSrs[srsIndex][srsLvl].color, srsDetailSrs[srsIndex][srsLvl].opacity);

                    var relatedCounts = filterBySRSCountOnAbbrev(countsBySRS, srsDetailSrs[srsIndex][srsLvl].srsAbbrev);
                    var textX = Math.cos((barStartAngle + parseInt(srsIndex) + (2 * parseInt(srsLvl) + 1) * 0.125 * (parseInt(srsIndex) + 1)) * arcWidth) * srsDetailTextRadius + canvasCenter.x;
                    var textY = Math.sin((barStartAngle + parseInt(srsIndex) + (2 * parseInt(srsLvl) + 1) * 0.125 * (parseInt(srsIndex) + 1)) * arcWidth) * srsDetailTextRadius + canvasCenter.y;
                    drawCenteredText(ctx, textX, textY, relatedCounts[0].total, helperFont);
                }
            }
        }
    }

    function drawSRSProgressLeechCounts(itemBreakdown) {
        if (!wkof.settings.db_cockpit.showLeechCount) {
            return;
        }

        var ctx = $('#speedometer')[0].getContext("2d");
        var countsBySRS = itemBreakdown.countBySRS.bySRSArray;

        var leechCount = 0;
        var sumFunction = function(countItem) {
            leechCount += countItem.leech;
        };

        var barStartAngle = srsStartAngle[parseInt(wkof.settings.db_cockpit.srsOrientation)];
        for(var srsLvl in srsDrawing) {
            if (srsDrawing[srsLvl].srsAbbrev == 'Bur') { continue; }
            var relatedCounts = filterBySRSCountOnAbbrev(countsBySRS, srsDrawing[srsLvl].srsAbbrev);
            leechCount = 0;
            relatedCounts.forEach(sumFunction);

            var textRadius = (3 * srsOutsideRadius + srsInsideRadius) / 4;
            var textX = Math.cos((barStartAngle + parseInt(srsLvl) + 0.875) * arcWidth) * textRadius + canvasCenter.x;
            var textY = Math.sin((barStartAngle + parseInt(srsLvl) + 0.875) * arcWidth) * textRadius + canvasCenter.y;
            var resultDimensions = drawCenteredText(ctx, textX, textY, leechCount, leechFont);

            createHighlightHotspot(canvasCenter.x, canvasCenter.y, srsOutsideRadius, srsInsideRadius,
                                   ((barStartAngle + parseInt(srsLvl))) * arcWidth, ((barStartAngle + parseInt(srsLvl) + 1)) * arcWidth, srsDrawing[srsLvl].color(), null,
                                   resultDimensions.minX, resultDimensions.minY, resultDimensions.width, resultDimensions.height,
                                   textX, textY, leechCount, leechFont);
        }

        // then draw it for the details if visible and only if non-zero
        if (wkof.settings.db_cockpit.showSrsDetail) {
            for (var srsIndex in srsDetailSrs) {
                var stage = parseInt(srsIndex);
                for(srsLvl in srsDetailSrs[srsIndex]) {
                    relatedCounts = filterBySRSCountOnAbbrev(countsBySRS, srsDetailSrs[srsIndex][srsLvl].srsAbbrev);
                    if (relatedCounts[0].leech == 0) { continue; }

                    textRadius = ((2 - stage) * srsOutsideDetailRadius + srsInsideDetailRadius) / (3 - stage);
                    textX = Math.cos((barStartAngle + stage + (parseInt(srsLvl) + 1) * 0.25 * (stage + 1) - ((stage + 1)/20)) * arcWidth) * textRadius + canvasCenter.x;
                    textY = Math.sin((barStartAngle + stage + (parseInt(srsLvl) + 1) * 0.25 * (stage + 1) - ((stage + 1)/20)) * arcWidth) * textRadius + canvasCenter.y;
                    resultDimensions = drawCenteredText(ctx, textX, textY, relatedCounts[0].leech, leechFont);

                    createHighlightHotspot(canvasCenter.x, canvasCenter.y, srsOutsideDetailRadius, srsInsideDetailRadius,
                                           (barStartAngle + parseInt(srsIndex) + parseInt(srsLvl) * 0.25 * (parseInt(srsIndex) + 1)) * arcWidth,
                                           (barStartAngle + parseInt(srsIndex) + (parseInt(srsLvl) + 1) * 0.25 * (parseInt(srsIndex) + 1)) * arcWidth,
                                           srsDetailSrs[srsIndex][srsLvl].color, srsDetailSrs[srsIndex][srsLvl].opacity,
                                           resultDimensions.minX, resultDimensions.minY, resultDimensions.width, resultDimensions.height,
                                           textX, textY, relatedCounts[0].leech, leechFont);
                }
            }
        }
    }

    // level by SRS
    function drawSRSProgressLevel(itemBreakdown) {
        var showSrsLevelBar = parseInt(wkof.settings.db_cockpit.showSrsLevelBar);
        switch (showSrsLevelBar) {
            case 0:
                // no display
                break;
            case 1:
                // level indications per SRS stage
                drawSRSProgressLevelBySrs(itemBreakdown);
                break;
            case 2:
                // level arrowheads
                drawSRSProgressLevelDials(itemBreakdown);
                break;
        }
    }

    function drawSRSProgressLevelBySrs(itemBreakdown) {
        var ctx = $('#speedometer')[0].getContext("2d");
        var barStartAngle = srsStartAngle[parseInt(wkof.settings.db_cockpit.srsOrientation)];

        // level indications per SRS stage
        drawSRSArc(ctx, canvasCenter.x, canvasCenter.y, srsOutsideLevelRadius , srsInsideLevelRadius,
                   barStartAngle * arcWidth, (barStartAngle - 1) * arcWidth, srsLevelColors.bgColor);

        var lvlIndex = 1;
        var totalCount = 0;
        var sumFunction = function(countItem) { totalCount += countItem.total; };
        var fractionFinished = wkof.settings.db_cockpit.srsLevelThreshold/100;

        for(var srsIndex = srsDrawing.length - 1; srsIndex >= 0; srsIndex--) {
            var srsLevel = srsDrawing[srsIndex];

            FIND_LEVEL_LOOP: while(true) {

                var textX = Math.cos((barStartAngle + parseInt(srsIndex) + 0.5) * arcWidth) * srsLevelTextRadius + canvasCenter.x;
                var textY = Math.sin((barStartAngle + parseInt(srsIndex) + 0.5) * arcWidth) * srsLevelTextRadius + canvasCenter.y;
                if (!itemBreakdown.countByLevel[lvlIndex]) {
                    // we've run out of levels, paint the last one and be done
                    drawCenteredText(ctx, textX, textY, "L"+(lvlIndex-1), helperFont);
                    createHotspot(textX, textY, helperFont.fontSize, '0% to level ' + lvlIndex);
                    break FIND_LEVEL_LOOP;
                }

                // select all elements of the countbySRS for the current level that have passed the current srs stage (note: bySRSArray is off by 1 due to locked count)
                var relatedCounts = itemBreakdown.countByLevel[lvlIndex].bySRSArray.slice(srsLevel.srsLevel+1, itemBreakdown.countByLevel[lvlIndex].bySRSArray.length);
                totalCount = 0;
                relatedCounts.forEach(sumFunction);

                if (totalCount / itemBreakdown.countByLevel[lvlIndex].total >= fractionFinished) {
                    // go up a level
                    lvlIndex++;
                }
                else {
                    // previous level was our max
                    drawCenteredText(ctx, textX, textY, "L"+(lvlIndex-1), helperFont);
                    var tooltip = Math.floor(100*totalCount/itemBreakdown.countByLevel[lvlIndex].total) + '% to level ' + lvlIndex + ' (' + totalCount + ' of ' + itemBreakdown.countByLevel[lvlIndex].total + ')';
                    createHotspot(textX, textY, helperFont.fontSize, tooltip);
                    break FIND_LEVEL_LOOP;
                }
            }
        }
    }

    function getLevelCounts(remainingItems) {
        var levelCounts = { passed: 0, total: 0, radical: 0, kanji: 0, vocabulary: 0 };
        levelCounts.passed = remainingItems;
        return levelCounts;
    }

    function createDialDivId(level) {
        return "ct_dial" + level;
    }

    function drawSRSProgressLevelDials(itemBreakdown) {
        var ctx = $('#speedometer')[0].getContext("2d");
        var barStartAngle = srsStartAngle[parseInt(wkof.settings.db_cockpit.srsOrientation)];

        // level arrowheads
        drawSRSArc(ctx, canvasCenter.x, canvasCenter.y, srsOutsideLevelRadius , srsInsideLevelRadius,
                   barStartAngle * arcWidth, (barStartAngle - 1.5) * arcWidth, srsLevelColors.bgColor);

        if (wkof.settings.db_cockpit.showLevelGridLines) {
            // draw target dashes
            for(var srsLvl in srsDrawing) {
                var dashAngle = (barStartAngle + parseInt(srsLvl) + 0.5) * arcWidth;
                var startX = Math.cos(dashAngle) * (srsInsideRadius - dashLength*3/4) + canvasCenter.x;
                var startY = Math.sin(dashAngle) * (srsInsideRadius - dashLength*3/4) + canvasCenter.y;
                var endX = Math.cos(dashAngle) * (srsInsideRadius + dashLength/4) + canvasCenter.x;
                var endY = Math.sin(dashAngle) * (srsInsideRadius + dashLength/4) + canvasCenter.y;

                drawLine(ctx, { x: startX, y: startY }, { x: endX, y: endY }, dashFont);
            }
        }

        var previousDialScores = [];
        // get the minimum dial separation from the settings, but enforce 0-10% range
        var minimumSeparation = Math.min(3.6, Math.max(0, wkof.settings.db_cockpit.srsLevelSeparation * 0.36));

        var levelCounts = getLevelCounts(0);
        var levelFunction = function(countItem) {
            levelCounts.total += countItem.total;
            levelCounts.radical += countItem.radical;
            levelCounts.kanji += countItem.kanji;
            levelCounts.vocabulary += countItem.vocabulary;
        }
        var totalCount = 0;
        var totalFunction = function(countItem) { totalCount += countItem.total; };

        // first create a list of divs and cross link them (previous - next)
        var dialDivs = {};
        var previousLevel = null;
        for(var lvl = 1; lvl < itemBreakdown.countByLevel.length; lvl++) {

            // check if the level requires a dial div
            var averageScore = itemBreakdown.countByLevel[lvl].progressScore / itemBreakdown.countByLevel[lvl].total;
            if ((averageScore == 0) || (averageScore == 36)) {
                // don't paint unstarted or completely burnt
                continue;
            }

            dialDivs[lvl] = {
                previousLevel: previousLevel,
                nextLevel: null
            };
            if (dialDivs[previousLevel]) {
                dialDivs[previousLevel].nextLevel = lvl;
            }
            previousLevel = lvl;
        }

        LEVEL_LOOP: for(lvl = 1; lvl < itemBreakdown.countByLevel.length; lvl++) {

            // check if the level requires a dial
            averageScore = itemBreakdown.countByLevel[lvl].progressScore / itemBreakdown.countByLevel[lvl].total;
            if ((averageScore == 0) || (averageScore == 36)) {
                // don't paint unstarted or completely burnt
                continue LEVEL_LOOP;
            }

            // create the dial div
            var levelData = {
                level: lvl,
                score: (100*averageScore/36).toFixed(1)
            };
            var relatedCounts = itemBreakdown.countByLevel[lvl].bySRSArray.slice(0,2);
            relatedCounts.forEach(levelFunction);
            var nextSrsCount = getLevelCounts(levelCounts.passed - levelCounts.total);
            levelData.unstarted = levelCounts;
            levelCounts = nextSrsCount;
            for(srsLvl in srsDrawing) {
                relatedCounts = filterBySRSCountOnAbbrev(itemBreakdown.countByLevel[lvl].bySRSArray, srsDrawing[srsLvl].srsAbbrev);
                relatedCounts.forEach(levelFunction);
                nextSrsCount = getLevelCounts(levelCounts.passed - levelCounts.total);
                levelCounts.passed = (100 * (levelCounts.passed + itemBreakdown.countByLevel[lvl].total) / itemBreakdown.countByLevel[lvl].total).toFixed(0);
                levelData[srsDrawing[srsLvl].srsAbbrev] = levelCounts;
                levelCounts = nextSrsCount;
            }
            // init for next level loop
            levelCounts = getLevelCounts(0);

            createDialDiv(levelData, dialDivs[lvl]);

            // check if this info needs a dial
            for (var previousDialScore in previousDialScores) {
                if (Math.abs(previousDialScores[previousDialScore] - averageScore) < minimumSeparation) {
                    // don't paint if too close to previous dials
                    continue LEVEL_LOOP;
                }
            }
            previousDialScores.push(averageScore);

            // calculate angle at which to draw
            var angle = arcWidth * (0.125 * averageScore + barStartAngle);
            var lvlTextX = Math.cos(angle) * levelProgressTextRadius + canvasCenter.x;
            var lvlTextY = Math.sin(angle) * levelProgressTextRadius + canvasCenter.y;

            // draw the dial and add the hover and click hotspots
            drawCenteredText(ctx, lvlTextX, lvlTextY, lvl, levelProgressFont);
            drawArrowHead(ctx, srsInsideRadius, angle, levelProgressFont.color, levelProgressFont.fontSize);
            createHotspot(lvlTextX, lvlTextY, levelProgressFont.fontSize, "L" + lvl + " progress " + (100*averageScore/36).toFixed(1) + "%");
            addDialHotspot(lvlTextX, lvlTextY, levelProgressFont.fontSize, levelData);
        }
    }

    function createLockedCountDiv() {
        if (!$('#lockedCount').length) {
            var lockedCount = document.createElement("div");
            lockedCount.id="lockedCount";
            lockedCount.className="db_cockpit_locked";
            $('#dbc_gas').append(lockedCount);
            $('#lockedCount').append(
                '<span id="lockedCountSpan"></span>'
              + '<span class="title">Locked</span>'
              + '<div class="db_cockpit_tooltipBreakdownText" style="top:60px">'
              +   '<div class="srs-logo apprentice"></div>'
              +   '<div class="tt_bd_totals"><ul>'
              +     '<li>rad:<span>0</span></li>'
              +     '<li>kan:<span>0</span></li>'
              +     '<li>voc:<span>0</span></li>'
              +   '</ul></div>'
              + '</div>'
            );
        }
        if (!$('#fuelgauge').length) {
            var fuelgaugeDiv = document.createElement("div");
            fuelgaugeDiv.id = "fuelgaugeDiv";
            fuelgaugeDiv.className = "db_cockpit_fuelgauge";
            $('#dbc_gas').append(fuelgaugeDiv);

            var fuelgauge = document.createElement("canvas");
            fuelgauge.id="fuelgauge";
            fuelgauge.width = 120;
            fuelgauge.height = 100;
            $('#fuelgaugeDiv').append(fuelgauge);
            $('#fuelgaugeDiv').append(
            '<div class="db_cockpit_tooltipText">'
          +   '<table><tr>'
          +     '<td style="width:80px">radicals</td><td style="align:right;">0</td>'
          +     '</tr><tr>'
          +     '<td style="width:80px">kanji</td><td>0</td>'
          +     '</tr><tr>'
          +     '<td style="width:80px">vocabulary</td><td>0</td>'
          +     '</tr>'
          +   '<table>'
          + '</div>'
        );

        }
    }

    function createLockedCount(itemBreakdown) {
        createLockedCountDiv();
        var showSrsLocked = parseInt(wkof.settings.db_cockpit.showSrsLocked);
        switch (showSrsLocked) {
            case 1:
                $('#lockedCountSpan').html(lockedCount.total);
                $('#lockedCount .tt_bd_totals li:first-child span').html(lockedCount.radical);
                $('#lockedCount .tt_bd_totals li:nth-child(2) span').html(lockedCount.kanji);
                $('#lockedCount .tt_bd_totals li:last-child span').html(lockedCount.vocabulary);
                break;
            default: // case 2:
                drawFuelGauge(itemBreakdown);
                $('#fuelgaugeDiv table tr:first-child td:last-child').html(lockedCount.radical);
                $('#fuelgaugeDiv table tr:nth-child(2) td:last-child').html(lockedCount.kanji);
                $('#fuelgaugeDiv table tr:last-child td:last-child').html(lockedCount.vocabulary);
                break;
        }
    }

    var FUELGAUGE_SIZE = { width: 120, height: 100 };
    var FUELGAUGE_DIAL = {
        x: FUELGAUGE_SIZE.width/2 - 5,
        y: FUELGAUGE_SIZE.height/2 + 1,
        radius: FUELGAUGE_SIZE.height/2 - 10,
        centerRadius: 5,
        angleFull: 10 * Math.PI / 6,
        angleEmpty: 13 * Math.PI / 6
    };
    var FUELGAUGE_ICON = {
        x: 34,
        y: 22,
        size: 16
    };
    var FUELGAUGE_NR = {
        x: 51,
        y: 74,
        font: {
            color: '#ffffff',
            fontStyle: "bold 20px Arial, sans-serif",
            fontSize: calculateFontSize("bold 20px Arial, sans-serif")
        }
    }

    function drawFuelGauge(itemBreakdown) {
        emptyFuelGauge();
        var ctx = $('#fuelgauge')[0].getContext("2d");
        ctx.save();

        // dial for locked items
        var totalItems = lockedCount.total + itemBreakdown.countBySRS.total - filterBySRSCountOnAbbrev(itemBreakdown.countBySRS.bySRSArray, "Loc")[0].total;
        var dialAngle = FUELGAUGE_DIAL.angleEmpty - (Math.abs(FUELGAUGE_DIAL.angleFull - FUELGAUGE_DIAL.angleEmpty) * lockedCount.total / totalItems);
        var dialLeftLocation = {
            x: Math.cos(dialAngle - Math.PI/180) * (FUELGAUGE_DIAL.radius + 5) + FUELGAUGE_DIAL.x,
            y: Math.sin(dialAngle - Math.PI/180) * (FUELGAUGE_DIAL.radius + 5) + FUELGAUGE_DIAL.y
        };
        var dialRightLocation = {
            x: Math.cos(dialAngle + Math.PI/180) * (FUELGAUGE_DIAL.radius + 5) + FUELGAUGE_DIAL.x,
            y: Math.sin(dialAngle + Math.PI/180) * (FUELGAUGE_DIAL.radius + 5) + FUELGAUGE_DIAL.y
        };
        var leftAttach = {
            x: Math.cos(dialAngle - Math.PI / 2) * FUELGAUGE_DIAL.centerRadius + FUELGAUGE_DIAL.x,
            y: Math.sin(dialAngle - Math.PI / 2) * FUELGAUGE_DIAL.centerRadius + FUELGAUGE_DIAL.y
        };
        var rightAttach = {
            x: Math.cos(dialAngle + Math.PI / 2) * FUELGAUGE_DIAL.centerRadius + FUELGAUGE_DIAL.x,
            y: Math.sin(dialAngle + Math.PI / 2) * FUELGAUGE_DIAL.centerRadius + FUELGAUGE_DIAL.y
        };

        var ctxGradient = ctx.createRadialGradient(
            FUELGAUGE_DIAL.x,
            FUELGAUGE_DIAL.y,
            5,
            FUELGAUGE_DIAL.x,
            FUELGAUGE_DIAL.y,
            FUELGAUGE_DIAL.radius - 5
        );

        ctxGradient.addColorStop(0, "#882d9e");
        ctxGradient.addColorStop(1, "#aa38c6");

        ctx.fillStyle = ctxGradient;
        ctx.beginPath();
        ctx.moveTo(leftAttach.x, leftAttach.y);
        ctx.lineTo(dialLeftLocation.x, dialLeftLocation.y);
        ctx.lineTo(dialRightLocation.x, dialRightLocation.y);
        ctx.lineTo(rightAttach.x, rightAttach.y);
        ctx.closePath();
        ctx.fill();

        // dial center to cover the nasty end of the dial
        var centerGradient = ctx.createRadialGradient(
            FUELGAUGE_DIAL.x - 2,
            FUELGAUGE_DIAL.y - 2,
            0,
            FUELGAUGE_DIAL.x - 2,
            FUELGAUGE_DIAL.y - 2,
            FUELGAUGE_DIAL.centerRadius + 3
        );

        centerGradient.addColorStop(0, "#999999");
        centerGradient.addColorStop(1, "#666666");

        ctx.fillStyle = centerGradient;
	    ctx.beginPath();
	    ctx.arc(FUELGAUGE_DIAL.x, FUELGAUGE_DIAL.y, FUELGAUGE_DIAL.centerRadius, 0, 2 * Math.PI);
	    ctx.fill();

        ctx.restore();

        // add the locked count
        drawCenteredText(ctx, FUELGAUGE_NR.x, FUELGAUGE_NR.y, lockedCount.total, FUELGAUGE_NR.font);
    }

    function emptyFuelGauge() {
        var ctx = $('#fuelgauge')[0].getContext("2d");
        ctx.clearRect(0, 0, FUELGAUGE_SIZE.width, FUELGAUGE_SIZE.height);
        ctx.save();

        // background
        ctx.fillStyle = '#bbbbbb';
	    ctx.beginPath();
	    ctx.arc(FUELGAUGE_SIZE.width/2, FUELGAUGE_SIZE.height/2, FUELGAUGE_SIZE.height/2 - 2, 0, 2 * Math.PI);
	    ctx.fill();

        // rim
        ctx.strokeStyle = '#666666';
        ctx.lineWidth = 3;
        ctx.beginPath();
	    ctx.arc(FUELGAUGE_SIZE.width/2, FUELGAUGE_SIZE.height/2, FUELGAUGE_SIZE.height/2 - 2, 0, 2 * Math.PI);
        ctx.stroke();

        // scale
        ctx.strokeStyle = '#666666';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.arc(FUELGAUGE_DIAL.x, FUELGAUGE_DIAL.y, FUELGAUGE_DIAL.radius, FUELGAUGE_DIAL.angleFull, FUELGAUGE_DIAL.angleEmpty);
        ctx.stroke();

        // indicator markers
        // full
        ctx.beginPath();
        ctx.moveTo(Math.cos(FUELGAUGE_DIAL.angleFull) * (FUELGAUGE_DIAL.radius - 5) + FUELGAUGE_DIAL.x, Math.sin(FUELGAUGE_DIAL.angleFull) * (FUELGAUGE_DIAL.radius - 5) + FUELGAUGE_DIAL.y);
        ctx.lineTo(Math.cos(FUELGAUGE_DIAL.angleFull) * (FUELGAUGE_DIAL.radius + 5) + FUELGAUGE_DIAL.x, Math.sin(FUELGAUGE_DIAL.angleFull) * (FUELGAUGE_DIAL.radius + 5) + FUELGAUGE_DIAL.y);
        ctx.stroke();

        // 3/4
        ctx.beginPath();
        ctx.moveTo(Math.cos((3 * FUELGAUGE_DIAL.angleFull + FUELGAUGE_DIAL.angleEmpty) / 4) * (FUELGAUGE_DIAL.radius - 3) + FUELGAUGE_DIAL.x, Math.sin((3 * FUELGAUGE_DIAL.angleFull + FUELGAUGE_DIAL.angleEmpty) / 4) * (FUELGAUGE_DIAL.radius - 3) + FUELGAUGE_DIAL.y);
        ctx.lineTo(Math.cos((3 * FUELGAUGE_DIAL.angleFull + FUELGAUGE_DIAL.angleEmpty) / 4) * (FUELGAUGE_DIAL.radius + 0) + FUELGAUGE_DIAL.x, Math.sin((3 * FUELGAUGE_DIAL.angleFull + FUELGAUGE_DIAL.angleEmpty) / 4) * (FUELGAUGE_DIAL.radius + 0) + FUELGAUGE_DIAL.y);
        ctx.stroke();

        // 1/2
        ctx.beginPath();
        ctx.moveTo(Math.cos(23 * Math.PI / 12) * (FUELGAUGE_DIAL.radius - 3) + FUELGAUGE_DIAL.x, Math.sin(23 * Math.PI / 12) * (FUELGAUGE_DIAL.radius - 3) + FUELGAUGE_DIAL.y);
        ctx.lineTo(Math.cos(23 * Math.PI / 12) * (FUELGAUGE_DIAL.radius + 3) + FUELGAUGE_DIAL.x, Math.sin(23 * Math.PI / 12) * (FUELGAUGE_DIAL.radius + 3) + FUELGAUGE_DIAL.y);
        ctx.stroke();

        // 1/4
        ctx.beginPath();
        ctx.moveTo(Math.cos((FUELGAUGE_DIAL.angleFull + 3 * FUELGAUGE_DIAL.angleEmpty) / 4) * (FUELGAUGE_DIAL.radius - 3) + FUELGAUGE_DIAL.x, Math.sin((FUELGAUGE_DIAL.angleFull + 3 * FUELGAUGE_DIAL.angleEmpty) / 4) * (FUELGAUGE_DIAL.radius - 3) + FUELGAUGE_DIAL.y);
        ctx.lineTo(Math.cos((FUELGAUGE_DIAL.angleFull + 3 * FUELGAUGE_DIAL.angleEmpty) / 4) * (FUELGAUGE_DIAL.radius + 0) + FUELGAUGE_DIAL.x, Math.sin((FUELGAUGE_DIAL.angleFull + 3 * FUELGAUGE_DIAL.angleEmpty) / 4) * (FUELGAUGE_DIAL.radius + 0) + FUELGAUGE_DIAL.y);
        ctx.stroke();

        // empty
        ctx.beginPath();
        ctx.moveTo(Math.cos(FUELGAUGE_DIAL.angleEmpty) * (FUELGAUGE_DIAL.radius - 5) + FUELGAUGE_DIAL.x, Math.sin(FUELGAUGE_DIAL.angleEmpty) * (FUELGAUGE_DIAL.radius - 5) + FUELGAUGE_DIAL.y);
        ctx.lineTo(Math.cos(FUELGAUGE_DIAL.angleEmpty) * (FUELGAUGE_DIAL.radius + 5) + FUELGAUGE_DIAL.x, Math.sin(FUELGAUGE_DIAL.angleEmpty) * (FUELGAUGE_DIAL.radius + 5) + FUELGAUGE_DIAL.y);
        ctx.stroke();

        // indicators
        var indicatorFont = {
            color: '#666666',
            fontStyle: "10px Arial, sans-serif",
            fontSize: 10
        };
        drawCenteredText(ctx, Math.cos(FUELGAUGE_DIAL.angleFull - Math.PI / 12) * FUELGAUGE_DIAL.radius + FUELGAUGE_DIAL.x, Math.sin(FUELGAUGE_DIAL.angleFull - Math.PI / 12) * FUELGAUGE_DIAL.radius + FUELGAUGE_DIAL.y, "F", indicatorFont);
        drawCenteredText(ctx, Math.cos(FUELGAUGE_DIAL.angleEmpty + Math.PI / 12) * FUELGAUGE_DIAL.radius + FUELGAUGE_DIAL.x, Math.sin(FUELGAUGE_DIAL.angleEmpty + Math.PI / 12) * FUELGAUGE_DIAL.radius + FUELGAUGE_DIAL.y, "E", indicatorFont);

        // icon
        ctx.fillStyle = '#888888';
        ctx.strokeStyle = '#666666';
        // head
        ctx.beginPath();
        ctx.moveTo(41, 22);
        ctx.lineTo(50, 22);
        ctx.lineTo(50, 28);
        ctx.lineTo(41, 28);
        ctx.lineTo(41, 22);
        ctx.stroke();

        // body
        ctx.fillRect(42, 28, 8, 9);

        // fuel line
        ctx.beginPath();
        ctx.moveTo(49, 31);
        ctx.lineTo(50, 31);
        ctx.lineTo(51, 32);
        ctx.lineTo(51, 35);
        ctx.lineTo(52, 35);
        ctx.lineTo(53, 34);
        ctx.lineTo(53, 26);
        ctx.lineTo(52, 26);
        ctx.lineTo(52, 25);
        ctx.lineTo(50, 25);
        ctx.stroke();

        ctx.restore();
    }

    function createTotalProgressDiv() {
        if (!$('#totalProgressBar').length) {
            var totalProgressBar = document.createElement("div");
            totalProgressBar.id="totalProgressBar";
            totalProgressBar.className='hidden';
            $('#dbc_bar60').append(totalProgressBar);
            $('#totalProgressBar').css("z-index", "0");
        }
    }

    function createTotalProgressIndication(itemBreakdown) {
        createTotalProgressDiv();
        populateTotalProgressIndication(itemBreakdown);
    }

    function populateTotalProgressIndication(itemBreakdown) {
        // emtpy the bar
        $('#totalProgressBar').html('');

        // sum all locked items, all items up to the current level and subtract the current level locked items
        var allItems = lockedCount.total + itemBreakdown.countBySRS.total - itemBreakdown.countBySRS.bySRSArray[0].total;
        // unstarted is locked and init (ready for lessons)
        var totalUnstarted = lockedCount.total + itemBreakdown.countBySRS.bySRSArray[1].total;

        var totalCount = 0;
        var cumulativePercentage = 0;
        var sumFunction = function(countItem) {
            totalCount += countItem.total;
        };
        var zindex = 7;
        var previousName;

        // burn -> apprentice only if not reversed
        if(!wkof.settings.db_cockpit.reverseTotalProgress) {
            for(var srsLvl = srsDrawing.length - 1; srsLvl >= 0; srsLvl--) {
                var relatedCounts = filterBySRSCountOnAbbrev(itemBreakdown.countBySRS.bySRSArray, srsDrawing[srsLvl].srsAbbrev);
                totalCount = 0;
                relatedCounts.forEach(sumFunction);

                var percentage = (100 * totalCount / allItems).toFixed(4);
                createSrsProgressBar(srsDrawing[srsLvl].srsName, srsDrawing[srsLvl].color(), srsDrawing[srsLvl].color().progressText, percentage, cumulativePercentage, zindex--, previousName);
                cumulativePercentage += parseFloat(percentage);
                previousName = srsDrawing[srsLvl].srsName;
            }

            // and add the srs-unstaged part
            if (totalUnstarted > 0) {
                // draw it as 'the rest' to prevent white horizontal line due to rounding differences
                var remainingPercentage = 100 - cumulativePercentage;
                createSrsProgressBar("Locked/Lessons", { start: "#545454", end: "#545454" }, "#efefef", remainingPercentage, cumulativePercentage, zindex--, previousName);
            }
        }

        // apprentice -> burn only if reversed
        if(wkof.settings.db_cockpit.reverseTotalProgress) {
            // the srs-unstaged part
            var unstartedPercentage = (100 * totalUnstarted / allItems).toFixed(4);
            createSrsProgressBar("Locked/Lessons", { start: "#545454", end: "#545454" }, "#efefef", unstartedPercentage, cumulativePercentage, zindex--, previousName);
            cumulativePercentage += parseFloat(unstartedPercentage);
            previousName = "Locked/Lessons";

            for(srsLvl = 0; srsLvl < srsDrawing.length - 1; srsLvl++) {
                relatedCounts = filterBySRSCountOnAbbrev(itemBreakdown.countBySRS.bySRSArray, srsDrawing[srsLvl].srsAbbrev);
                totalCount = 0;
                relatedCounts.forEach(sumFunction);

                percentage = (100 * totalCount / allItems).toFixed(4);
                createSrsProgressBar(srsDrawing[srsLvl].srsName, srsDrawing[srsLvl].color(), srsDrawing[srsLvl].color().progressText, percentage, cumulativePercentage, zindex--, previousName);
                cumulativePercentage += parseFloat(percentage);
                previousName = srsDrawing[srsLvl].srsName;
            }

            // and add the burnt part as 'the rest'
            srsLvl = srsDrawing.length - 1;
            relatedCounts = filterBySRSCountOnAbbrev(itemBreakdown.countBySRS.bySRSArray, srsDrawing[srsLvl].srsAbbrev);
            totalCount = 0;
            relatedCounts.forEach(sumFunction);

            if (totalCount > 0) {
                remainingPercentage = 100 - cumulativePercentage;
                // draw it as 'the rest' to prevent white horizontal line due to rounding differences
                createSrsProgressBar(srsDrawing[srsLvl].srsName, srsDrawing[srsLvl].color(), srsDrawing[srsLvl].color().progressText, remainingPercentage, cumulativePercentage, zindex--, previousName);
            }
        }
    }

    var timeout = {};
    function createSrsProgressBar(name, color, altTextColor, percentage, offsetLeft, zindex, previousName) {

        // bloody pass as string, give me the numbers
        var nrPercentage = parseFloat(percentage);
        var nrOffset = parseFloat(offsetLeft);
        var totalWidth = ((nrPercentage + nrOffset));
        var idName = name.slice(0,3);

        var textColor = '#efefef';
        if (altTextColor != undefined) {
            textColor = altTextColor;
        }

        $('#totalProgressBar').append(
            '<div id="tpb_' + idName + '" title="' + name + ': ' + nrPercentage.toFixed(2)
            + '%" style="width: ' + totalWidth
            + '%; background-image: linear-gradient(' + color.start + ', ' + color.end
            + '); color: ' + textColor
            + '; margin-left: 0; z-index: ' + zindex
            + ';"><span style="margin-left: ' + (totalWidth == 0 ? 0 : (100 * nrOffset / totalWidth))
            + '%; align: center;" displayvalues=":' + nrPercentage.toFixed(2) + '%_:' + nrPercentage.toFixed(1)
            + '%_0:&nbsp;">$nbsp;</span></div>');

        $('#cockpitPanel')[0].addEventListener('dashboard-cockpit-loaded', e => window.db_cockpit_calcAndDisplayTotalProgressBarPercentage('tpb_' + idName, previousName ? 'tpb_' + previousName.slice(0,3) : null));
        timeout[idName] = false;
        window.addEventListener('resize', function() {
            clearTimeout(timeout[''+idName]);
            timeout[''+idName] = setTimeout(() => window.db_cockpit_displayTotalProgressBarPercentage('tpb_' + idName, previousName ? 'tpb_' + previousName.slice(0,3) : null), 250);
        });
    }

    function calcAndDisplayTotalProgressBarPercentage(divId, blockingDivId) {
        var tpbSpan = $('#' + divId + ' span')[0];
        var original = tpbSpan.getAttribute("displayvalues");
        var sizeValues = tpbSpan.getAttribute("displayvalues").split("_").map(stringValue => stringValue.split(":"));
        if (sizeValues[0][0] == '') {
            tpbSpan.innerHTML = sizeValues[0][1];
            sizeValues[0][0] = tpbSpan.getBoundingClientRect().width;

            tpbSpan.innerHTML = sizeValues[1][1];
            sizeValues[1][0] = tpbSpan.getBoundingClientRect().width;

            tpbSpan.setAttribute("displayvalues", sizeValues.map(sizeValue => sizeValue.join(":")).join("_"));
        }

        window.db_cockpit_displayTotalProgressBarPercentage(divId, blockingDivId);
    }
    window.db_cockpit_calcAndDisplayTotalProgressBarPercentage = calcAndDisplayTotalProgressBarPercentage;

    function displayTotalProgressBarPercentage(divId, blockingDivId) {
        var tpbDiv = $('#' + divId)[0];
        var tbpSpan = $('#' + divId + ' span')[0];
        var sizeValues = tbpSpan.getAttribute("displayvalues").split("_").map(stringValue => stringValue.split(":"));
        if (sizeValues[0][0] == '') {
            // not ready to display
            return;
        }

        var subtractSize = 0;
        if (blockingDivId) {
            var tpbBlockingDiv = $('#' + blockingDivId);
            subtractSize = $('#' + blockingDivId)[0].getBoundingClientRect().width;
        }
        var availableSize = $('#' + divId)[0].getBoundingClientRect().width - subtractSize;
        availableSize = Math.max(availableSize, 0);

        for (var i = 0; i < sizeValues.length; i++) {
            if (sizeValues[i][0] <= availableSize) {
                tbpSpan.innerHTML = sizeValues[i][1];
                return;
            }
        }
    }
    window.db_cockpit_displayTotalProgressBarPercentage = displayTotalProgressBarPercentage;

    function displayGrid(allPassed) {
        resetGridDefaults(allPassed);
        structureGrid(allPassed);
        showOrHidePanels(allPassed);
    }

    function resetGridDefaults(allPassed) {
        //  default layout:
        //  -------------------|------------------|------------------|-------------------
        //  | dbc_gas          | dbc_core                            | dbc_cruise       |
        //  |-------------------                                     -------------------|
        //  | dbc_currentLevel |                                     | dbc_forecast     |
        //  --------------------------------------|-------------------                  -
        //  | dbc_bar60                                              |                  |
        //  -------------------|------------------|-------------------                  -
        //                                                           |                  |
        //                                                           |-------------------

        // GRID CELL PLACEMENTS
        $('#dbc_bar60').css('grid-row-start', '4');
        $('#dbc_core').css('grid-row-start', '1');
        $('#dbc_currentLevel').css('grid-row-start', '2');
        $('#dbc_currentLevel').css('grid-row-end', '3');
        $('#dbc_forecast').css('grid-row-start', '2');
        $('#dbc_forecast').css('grid-row-end', '5');
        $('#dbc_bar60').css('grid-column-start', '1');
        $('#dbc_bar60').css('grid-column-end', 'span 3');
        $('#dbc_bar60').css('grid-row-start', '3');

        // GRID CELL VISIBILITY
        $('#dbc_gas').removeClass('hidden');
        $('#dbc_cruise').removeClass('hidden');

        // GRID SIZING AND RESIZING
        $('#cockpitPanel').css('grid-template-columns', 'auto ' + getCanvasGridWidth() + 'px ' + getCanvasGridWidth() + 'px ' + BASE_COLUMN_WIDTH + 'px');
        $('#cockpitPanel').css('min-width', ((getLeftColumnWidth(allPassed) + 30 + getCanvasSize() + BASE_COLUMN_WIDTH)));   // left column, 3xmargin(10), canvas and forecast (335)

        // MOVE CELL CONTENTS
        var reviewButtonBlock = $('#reviewButtonBlock').detach();
        $('#dbc_cruise').append(reviewButtonBlock);
        if (isRSPresent()) {
            var reviewSummaryDiv = $('#reviewSummaryDiv').detach();
            $('#dbc_cruise').append(reviewSummaryDiv);
        }

        // MARGINS AND STYLING
        $('#dbc_core').css('margin-top', '');
        $('#dbc_bar60').css('margin-top', '');

        reviewButtonBlock.css("float", "left");
        reviewButtonBlock.css("margin-left", "0px");

        $('#dbc_forecast').css('min-height', '');
        $('.wk-panel--review-forecast').height("56.5%");

        if (moveExtraStudy()) {
            var recentMistakes = $('.wk-panel--recent-mistakes');
            recentMistakes.css("margin-bottom", "10px");
            var extraStudy = $('.wk-panel--extra-study');
            extraStudy.detach();
            extraStudy.css('max-width', '');
            extraStudy.css('margin-top', '');
            extraStudy.css('margin-bottom', '');
            var extraRecentStudy = $('#extraRecentStudy');
            extraRecentStudy.append(extraStudy);
        }
    }


    function structureGrid(allPassed) {

        var tpIndicator = parseInt(wkof.settings.db_cockpit.showTotalProgress);
        switch(tpIndicator) {
            case 0: // not visible
                // place in center beneath arc
                $('#dbc_bar60').css('grid-column-end', 'span 2');
                $('#dbc_bar60').css('grid-column-start', '2');
                $('#dbc_bar60').css('grid-row-start', '3');
                // and adjust size col heights
                $('#dbc_currentLevel').css('grid-row-end', '4');
                $('#dbc_forecast').css('grid-row-end', '4');
                break;
            case 1: // long bar with extended review forecast
                // is the default
                break;
            case 2: // short bar
                // place in center beneath arc
                $('#dbc_bar60').css('grid-column-end', 'span 2');
                $('#dbc_bar60').css('grid-column-start', '2');
                $('#dbc_bar60').css('grid-row-start', '3');
                // and adjust size col heights
                $('#dbc_currentLevel').css('grid-row-end', '4');
                break;
            case 3: // long bar with extended level progress panel
                // shrink col height
                $('#dbc_forecast').css('grid-row-end', '3');
                // shift bar
                $('#dbc_bar60').css('grid-column-start', '2');
                // and adjust size col heights
                $('#dbc_currentLevel').css('grid-row-end', '5');
                break;
            case 4: // full bar over whole screen width
                // shrink col height
                $('#dbc_forecast').css('grid-row-end', '3');
                // extend bar
                $('#dbc_bar60').css('grid-column-end', 'span 4');
                break;
            case 5: // top bar above the arc
                createTotalProgressDiv();
                // shift bar out of the way
                $('#dbc_bar60').css('grid-row-start', '4');
                // move arc down
                $('#dbc_core').css('grid-row-start', '2');
                $('#dbc_core').css('margin-top', '-50px');

                // position bar
                $('#dbc_bar60').css('grid-column-end', 'span 2');
                $('#dbc_bar60').css('grid-column-start', '2');
                $('#dbc_bar60').css('grid-row-start', '1');
                $('#dbc_bar60').css('margin-top', '4px');

                // and adjust col height
                $('#dbc_currentLevel').css('grid-row-end', '5');
                break;
        }

        var applyRightGridWidth = BASE_COLUMN_WIDTH + 10;
        var applyLeftGridWidth = getLeftColumnWidth(allPassed) + 10;

        if (!isRightGridVisible(allPassed)) {
            if (wkof.settings.db_cockpit.showMainButtons) {
                // 1. shift reviewsbutton to left of the speedometer
                var reviewButtonBlock = $('#reviewButtonBlock').detach();
                $('#dbc_gas').prepend(reviewButtonBlock);
                reviewButtonBlock.css("float", "right");
                reviewButtonBlock.css("margin-left", "10px");
            }
            if (isRSVisible()) {
                var reviewSummaryDiv = $('#reviewSummaryDiv').detach();
                $('#dbc_gas').prepend(reviewSummaryDiv);
                $('#reviewSummaryTile').css("margin-left", "10px");
            }

            // 2. restructure column
            applyRightGridWidth = 0;

            // also occupy the review forecast space so we fill out until the margin
            switch(tpIndicator) {
                case 1: // long bar with extended review forecast
                    $('#dbc_bar60').css('grid-column-end', 'span 4');
                    break;
                case 2: // short bar
                    $('#dbc_bar60').css('grid-column-end', 'span 3');
                    break;
                case 5: // top bar above the arc
                    $('#dbc_bar60').css('grid-column-end', 'span 3');
                    break;
            }
        }
        else if (!isTopRightCellVisible(allPassed)) {
            $('#dbc_cruise').addClass('hidden');
            $('#dbc_forecast').css('grid-row-start', '1');
        }

        if (!isLeftGridVisible(allPassed)) {
            // 1. restructure column
            applyLeftGridWidth = 0;

            // 2. hide dbc_gas because of the min width configurations
            $('#dbc_gas').addClass('hidden');

            // also occupy the level progress space so we fill out until the margin
            switch(tpIndicator) {
                case 2: // short bar
                    $('#dbc_bar60').css('grid-column-start', '1');
                    $('#dbc_bar60').css('grid-column-end', 'span 3');
                    break;
                case 3: // long bar with extended level progress panel
                    $('#dbc_bar60').css('grid-column-start', '1');
                    $('#dbc_bar60').css('grid-column-end', 'span 4');
                    break;
                case 5: // top bar above the arc
                    $('#dbc_bar60').css('grid-column-start', '1');
                    $('#dbc_bar60').css('grid-column-end', 'span 3');
                    break;
            }
        }
        else if (!isTopLeftCellVisible(allPassed)) {
            $('#dbc_gas').addClass('hidden');
            $('#dbc_currentLevel').css('grid-row-start', '1');
        }

        // restructure columns based on visibility
        $('#cockpitPanel').css('grid-template-columns', (applyLeftGridWidth > 0 ? 'auto ' : '0px ') + getCanvasGridWidth() + 'px ' + getCanvasGridWidth() + 'px ' + (applyRightGridWidth > 0 ? applyRightGridWidth-10 : 0) + 'px');
        $('#cockpitPanel').css('min-width', ((applyLeftGridWidth + 10 + getCanvasSize() + applyRightGridWidth)));   // left col, 2xmargin(10) and canvas

        var extraRecentStudy = $('#extraRecentStudy');
        if (moveExtraStudy()) {
            // place the new extra study panel
            var locationSetting = parseInt(getExtraStudyLocationSetting());
            var forecast = $('.wk-panel--review-forecast');
            var extraStudy = $('.wk-panel--extra-study');

            switch(locationSetting) {
                case 1: // default, above dashboard progress panel
                    extraRecentStudy.detach();
                    extraStudy.css('margin-bottom', '10px');
                    $('#dbc_currentLevel').prepend(extraRecentStudy);
                    break;
                case 2: // below main arc
                    extraRecentStudy.detach();
                    extraStudy.css('max-width', (getCanvasGridWidth() * 2 + 10) + 'px');
                    $('#dbc_core').append(extraRecentStudy);
                    break;
                case 3: // above review forecast
                    extraRecentStudy.detach();
                    extraStudy.css('margin-bottom', '10px');
                    forecast.height("267px");
                    $('#dbc_forecast').prepend(extraRecentStudy);
                    if (wkof.settings.db_cockpit.showForecast) {
                        $('#dbc_forecast').css('min-height', ((327 + extraStudy.outerHeight())) + 'px');
                    }
                    break;
                case 4: // below review forecast
                    extraRecentStudy.detach();
                    forecast.height("267px");
                    $('#dbc_forecast').append(extraRecentStudy);
                    if (wkof.settings.db_cockpit.showForecast) {
                        $('#dbc_forecast').css('min-height', ((327 + extraStudy.outerHeight())) + 'px');
                    }
                    break;
                case 5: // above main arc
                    extraRecentStudy.detach();
                    extraStudy.css('margin-bottom', '10px');
                    extraStudy.css('max-width', (getCanvasGridWidth() * 2 + 10) + 'px');
                    $('#dbc_core').css('margin-top', '');
                    $('#dbc_core').prepend(extraRecentStudy);
                    break;
                case 6: // below dashboard progress panel
                    extraRecentStudy.detach();
                    extraStudy.css('margin-top', '10px');
                    $('#dbc_currentLevel').append(extraRecentStudy);
                    break;
                case 7: // center top. This position DOES NOT EXIST in the cockpit itself, but only as a translation from ESM
                    extraRecentStudy.detach();
                    extraStudy.css('margin-bottom', '10px');
                    extraStudy.css('max-width', (getCanvasGridWidth() * 2 + 10) + 'px');
                    // if total progress bar setting is top, we put extra study above that
                    if (wkof.settings.db_cockpit.showTotalProgress == '5') {
                        $('#dbc_bar60').prepend(extraRecentStudy);
                        var shiftHeight = $('.wk-panel--extra-study').outerHeight() + $('#totalProgressBar').outerHeight() - 100 + 20;
                        $('#dbc_core').css('margin-top', shiftHeight + 'px');
                    }
                    // else above the arc (same as option 5)
                    else {
                        $('#dbc_core').prepend(extraRecentStudy);
                    }
                    break;
                case 8: // same as 1, but above recent mistakes
                    extraRecentStudy.detach();
                    $('#dbc_currentLevel').prepend(extraRecentStudy);
                    extraStudy.detach();
                    extraStudy.css('margin-bottom', '10px');
                    extraRecentStudy.prepend(extraStudy);
                    break;
            }
        }
        else {
            // still take care of recent mistakes, put it above level progress
            extraRecentStudy.detach();
            $('#dbc_currentLevel').prepend(extraRecentStudy);
        }
    }

    function showOrHidePanels(allPassed) {

        applyVisibility((wkof.settings.db_cockpit.showTotalProgress != '0'), $('#totalProgressBar'));
        applyVisibility(wkof.settings.db_cockpit.showForecast, $('.wk-panel--review-forecast'));
        applyVisibility(wkof.settings.db_cockpit.showMainButtons, $('#reviewButtonBlock'));
        applyVisibility(wkof.settings.db_cockpit.showReviewSummary, $('#reviewSummaryDiv'));

        applyVisibility(showLessonsButton(), $('.dashboard__lessons-and-reviews-section'));
        applyVisibility(showLockedCount('1'), $('#lockedCount'));
        applyVisibility(showLockedCount('2'), $('#fuelgaugeDiv'));
        applyVisibility(showLevelProgress(allPassed), $('.wk-panel--level-progress'));

        // place the new extra study panel if it exists and is not governed by the Extra study mover script
        if(moveExtraStudy()) {
            var locationSetting = parseInt(getExtraStudyLocationSetting());
            applyVisibility((locationSetting != 0), $('#extraRecentStudy'));
        }

        // 4 bottom panels, hide or show
        applyVisibility(wkof.settings.db_cockpit.showRecentUnlocks, $('.wk-panel--dashboard-list-unlocks'));
        applyVisibility(wkof.settings.db_cockpit.showCriticalItems, $('.wk-panel--dashboard-list-critical'));
        applyVisibility(wkof.settings.db_cockpit.showRecentBurns, $('.wk-panel--dashboard-list-burned'));
        applyVisibility(wkof.settings.db_cockpit.showForumTopics, $('.dashboard__community-banner'));
    }

    function applyVisibility(setting, panel) {
        if (setting) {
            panel.removeClass('hidden');
        }
        else {
            panel.addClass('hidden');
        }
    }

    // Adds the script's CSS to the page
    function add_css() {
        $('head').append(
            `<style id="db_cockpit_CSS">
                 #cockpitPanel {
                     display: grid; grid-template-columns: auto 19.5% 19.5% 29%; grid-template-rows: 240px auto; grid-gap: 10px 10px; margin-bottom: 10px;
                 }
                 #dbc_gas {
                     grid-column-start: 1; grid-column-end: 2; grid-row-start: 1; grid-row-end: 2;
                 }
                 #dbc_currentLevel {
                     grid-column-start: 1; grid-column-end: 2; grid-row-start: 2; grid-row-end: 3;
                 }
                 #dbc_core {
                     grid-column-start: 2; grid-column-end: span 2; grid-row-start: 1; grid-row-end: span 2; aligh: center; margin: 0 auto;
                 }
                 #dbc_bar60 {
                     grid-column-start: 1; grid-column-end: span 3; grid-row-start: 3; grid-row-end: span 1;
                 }
                 #dbc_cruise {
                     grid-column-start: 4; grid-column-end: 5; grid-row-start: 1; grid-row-end: 2;
                 }
                 #dbc_forecast {
                     grid-column-start: 4; grid-column-end: 5; grid-row-start: 2; grid-row-end: span 2;
                 }
                 .progression { display: block; }
                 .db_cockpit_locked {
                     height: 70px;
                     width: 120px;
                     position: relative;
                     float: left;
                     font: bold 24px Arial, sans-serif;
                     color: #fff;
                     background-color: #aaa;
                     padding-top: 30px;
                     text-align: center;
                 }
                 .db_cockpit_locked .db_cockpit_tooltipBreakdownText {
                     top: -60px;
                     left: -10px;
                 }
                 .db_cockpit_locked .title {
                     font-size: 14px;
                     font-weight: normal;
                     color: rgba(0,0,0,0.3);
                     text-shadow: none;
                     position: absolute;
                     bottom: 0;
                     left: 0;
                     width: 100%;
                     padding-bottom: 20px;
                     margin-bottom: 2.5px;
                 }
                 .db_cockpit_fuelgauge {
                     height: 100px;
                     width: 120px;
                     float: left;
                  }
                 .db_cockpit_tooltips { height: 0px; }
                 .db_cockpit_tooltipText {
                     visibility: hidden;
                     border: 1px solid #555;
                     background-color: #f8f8db;
                     color: #555;
                     font: 12px Segoe UI;
                     text-align: left;
                     padding: 3px 7px;
                     position: absolute;
                     z-index: 1;
                 }
                  .db_cockpit_fuelgauge .db_cockpit_tooltipText {
                     top: -80px;
                     left: 95px;
                     position: relative;
                  }
                  .db_cockpit_fuelgauge .db_cockpit_tooltipText tr td:last-child {
                     text-align: right;
                  }
                 .db_cockpit_tooltipBreakdownText {
                     visibility: hidden;
                     border: 3px solid rgba(75,75,75,0.8);
                     border-radius: 5px;
                     background-color: #333;
                     width: 150px;
                     color: rgba(255,255,255,0.7);
                     font: 14px Arial, sans-serif;
                     text-align: left;
                     padding: 0;
                     position: absolute;
                     z-index: 3;
                     display: grid; grid-template-columns: 50% 50%; grid-template-rows: 81px auto; grid-gap: 0px;
                 }
                 .db_cockpit_tooltipBreakdownText .srs-logo {
                     grid-column-start: 1; grid-column-end: 2; grid-row-start: 1; grid-row-end: 2;
                 }
                 .db_cockpit_tooltipBreakdownText .tt_bd_totals {
                     grid-column-start: 2; grid-column-end: 3; grid-row-start: 1; grid-row-end: 2;
                 }
                 .db_cockpit_tooltipBreakdownText ul {
                     list-style-type:none;
                     margin: 0 0 0 0;
                 }
                 .db_cockpit_tooltipBreakdownText li:first-child {
                     background-image: linear-gradient(to bottom, #0af, #0093dd);
                 }
                 .db_cockpit_tooltipBreakdownText li:nth-child(2) {
                     background-image: linear-gradient(to bottom, #f0a, #dd0093);
                 }
                 .db_cockpit_tooltipBreakdownText li:nth-child(3) {
                     background-image: linear-gradient(to bottom, #a0f, #9300dd);
                 }
                 .db_cockpit_tooltipBreakdownText li {
                     line-height: 27px;
                     padding-left: 5px;
                     padding-right: 5px;
                 }
                 .db_cockpit_tooltipBreakdownText li span {
                     font-weight: bold;
                     float: right;
                 }
                 .db_cockpit_tooltipDialText {
                     font: 12px Arial, sans-serif;
                     visibility: hidden;
                     border-radius: 5px;
                     background-color: #333;
                     color: #aaa;
                     text-align: right;
                     padding: 0;
                     position: absolute;
                     z-index: 2;
                 }
                 .db_cockpit_tooltipDialText table {
                     margin: 5px;
                 }
                 .db_cockpit_ctd_summary {
                     text-align: center;
                 }
                 .db_cockpit_tooltipDialText table tr td {
                     width: 40px;
                 }
                 .db_cockpit_ctd_level {
                     font: bold 36px Arial, sans-serif;
                 }
                 .db_cockpit_ctd_link {
                     font: 12px Arial, sans-serif;
                     vertical-align: middle;
                 }
                 .db_cockpit_ctd_clickable {
                     cursor: pointer;
                 }
                 .db_cockpit_ctd_score {
                     font: bold 16px Arial, sans-serif
                 }
                 .db_cockpit_ctd_header {
                     font-weight: bold;
                 }
                 .db_cockpit_ctd_category {
                     font-weight: bold;
                     text-align: left;
                 }
                 .db_cockpit_tooltipText_visible {
                     visibility: visible;
                 }
                 .db_cockpit_clicktipText_visible {
                     visibility: visible;
                 }
                 .db_cockpit_locked:hover .db_cockpit_tooltipBreakdownText {
                     visibility: visible;
                 }
                 .db_cockpit_fuelgauge:hover .db_cockpit_tooltipText {
                     visibility: visible;
                 }
                 #totalProgressBar {
                     height: 30px;
                     box-shadow: 0 0 2pt 2pt #BBBBBB;
                     position: relative;
                     width: 99.99%;
                 }
                 #totalProgressBar div {
                     height: 30px;
                     position: absolute;
                     text-align: center;
                     vertical-align: middle;
                     line-height: 30px;
                     color: #efefef;
                 }
             </style>`);
		}
    //-----------------------------------------------------------------------------------------------------------------------------------------------------//
    //------------------------------------------------------------------MENU AND SETTINGS------------------------------------------------------------------//
    //-----------------------------------------------------------------------------------------------------------------------------------------------------//

    function consoleLog(obj) {
        $.each(obj, function(key, element) {
            console.log('key: ' + key + ', value: ' + element);
        });
    }

    // Load settings and set defaults
    function load_settings() {
        var defaults = {
            srsOrientation: '0',
            srsSize: '0',
            goldenBurn: true,
            presetLevelColor: '0',
            levelNrInside: '#dd0093',
            levelNrOutside: '#0093dd',
            showSrsLocked: '2',
            showMainButtons: true,
            lvl60Lessons: false,
            lvl60Progress: false,

            showSrsDetail: false,
            showLeechCount: false,
            leechThreshold: 1,
            leechMinWrong: 1,
            leechDisplay: '0',
            showSrsLevelBar: '1',
            srsLevelThreshold: 90,
            srsLevelSeparation: 2,
            showLevelGridLines: false,
            showTotalProgress: '1',
            reverseTotalProgress: false,

            showForecast: true,
            showRecentUnlocks: false,
            showCriticalItems: true,
            showRecentBurns: true,
            showForumTopics: true,
            showExtraStudy: '1',
            showReviewSummary: true
        };
        return wkof.Settings.load('db_cockpit', defaults);
    }

    // Installs the options button in the menu
    function install_menu() {
        var config = {
            name: 'db_cockpit_settings',
            submenu: 'Settings',
            title: 'Dashboard Cockpit',
            on_click: open_settings
        };
        wkof.Menu.insert_script_link(config);
    }

    // Create the options
    function open_settings(items) {
        var config = {
            script_id: 'db_cockpit',
            title: 'Dashboard Cockpit Settings',
            on_save: fetch_and_update,
            content: {
                tabs: {
                    type: 'tabset',
                    content: {
                        srsProgressPage: {
                            type: 'page',
                            label: 'SRS Progress',
                            hover_tip: 'Settings for the SRS Progress display',
                            content: {
                                srsStyle_group: {
                                    type: 'group',
                                    label: 'Display Style',
                                    content: {
                                        srsOrientation: {
                                            type: 'dropdown',
                                            label: 'Orientation',
                                            hover_tip: 'Tilts the SRS progress overview',
                                            default: '0',
                                            content: {
                                               0: 'left',
                                               1: 'down'
                                            }
                                        },
                                        srsSize: {
                                            type: 'dropdown',
                                            label: 'Size',
                                            hover_tip: 'Size of the SRS progress arc',
                                            default: '0',
                                            content: {
                                               0: 'large',
                                               1: 'medium',
                                               2: 'small'
                                            }
                                        },
                                        goldenBurn: {
                                            type: 'checkbox',
                                            label: 'Golden Burn',
                                            hover_tip: 'Display golden Burn background',
                                            default: true
                                        },
                                        presetLevelColor: {
                                            type: 'dropdown',
                                            label: 'Level number display',
                                            hover_tip: 'Display method for the center level number',
                                            default: '0',
                                            content: {
                                                0: 'default (pale pink-blue)',
                                                1: 'pale with selected colors',
                                                2: 'bright with selected colors'
                                            }
                                        },
                                        levelNrInside: {
                                            type: 'color',
                                            label: 'Level center color',
                                            hover_tip: 'Color of the center of the level number',
                                            default: '#dd0093'
                                        },
                                        levelNrOutside: {
                                            type: 'color',
                                            label: 'Level outside color',
                                            hover_tip: 'Color the level number fades into on the edges',
                                            default: '#0093dd'
                                        }
                                    }
                                },
                                srsLocked_group: {
                                    type: 'group',
                                    label: 'Locked items',
                                    content: {
                                        showSrsLocked: {
                                            type: 'dropdown',
                                            label: 'Locked Count',
                                            hover_tip: 'Display the Locked count',
                                            default: '2',
                                            content: {
                                                0: 'none',
                                                1: 'Tile',
                                                2: 'Fuel Gauge (car)'
                                            }
                                        }
                                    }
                                },
                                lessonsReviews_group: {
                                    type: 'group',
                                    label: 'Lessons and Reviews buttons',
                                    content: {
                                        showMainButtons: {
                                            type: 'checkbox',
                                            label: 'Show lessons/reviews buttons',
                                            hover_tip: 'Provided for users running scripts that display alternate buttons. Cockpit does not move the buttons elsewhere!',
                                            default: true
                                        }
                                    }
                                },
                                level60_group: {
                                    type: 'group',
                                    label: 'Level 60 options',
                                    content: {
                                        lvl60Lessons: {
                                            type: 'checkbox',
                                            label: 'Auto hide lessons section',
                                            hover_tip: 'Hides the locked count and lessons button if neither has items left',
                                            default: false
                                        },
                                        lvl60Progress: {
                                            type: 'checkbox',
                                            label: 'Auto hide level progress',
                                            hover_tip: 'Hides the level progress panel when locked count and lessons are zero, and all items have reached Guru at least once (passed)',
                                            default: false
                                        }
                                    }
                                }
                            }
                        },
                        srsPluginsPage: {
                            type: 'page',
                            label: 'Progress Details',
                            hover_tip: 'Features that provide more details',
                            content: {
                                srsDetail_group: {
                                    type: 'group',
                                    label: 'SRS Details',
                                    content: {
                                        showSrsDetail: {
                                           type: 'checkbox',
                                           label: 'Show SRS Detail',
                                           hover_tip: 'Display the Apprentice and Guru detail splits',
                                           default: false
                                        },
                                        showLeechCount: {
                                           type: 'checkbox',
                                           label: 'Show Leech Count',
                                           hover_tip: 'Displays counts of the identified leeches per stage',
                                           default: false
                                        },
                                        leechThreshold: {
                                            type: 'number',
				                         	label: 'Leech threshold',
			                           		hover_tip: 'Higher threshold considers fewer items as leeches (1-4)',
			                         		default: 1
			                            },
                                        leechMinWrong: {
                                            type: 'number',
                                            label: 'Leech min. incorrect attempts',
                                            hover_tip: 'Consider only items with at least this many incorrect answers (1-10)',
                                            default: 1
                                        },
                                        leechDisplay: {
                                            type: 'dropdown',
                                            label: 'Brightness of Leech display',
                                            hover_tip: 'Select the desired brightness',
                                            default: '0',
                                            content: {
                                               0: 'dull',
                                               1: 'bright on hover',
                                               2: 'bright',
                                               3: 'inverted ellipse'
                                            }
                                        },
                                    }
                                },
                                srsLevel_group: {
                                    type: 'group',
                                    label: 'Level by SRS',
                                    content: {
                                        showSrsLevelBar: {
                                            type: 'dropdown',
                                            label: 'Display Level by SRS',
                                            hover_tip: 'Display user level SRS progression',
                                            default: '0',
                                            content: {
                                               0: 'none',
                                               1: 'Level by SRS stage',
                                               2: 'Level dials'
                                            }
                                        },
                                        srsLevelThreshold: {
                                            type: 'number',
				                         	label: 'Level by SRS threshold',
			                           		hover_tip: 'Percentage to consider level done',
			                         		default: 90
			                            },
                                        srsLevelSeparation: {
                                            type: 'number',
				                         	label: 'Level dials minimum separation',
			                           		hover_tip: 'Only display dials that are this far apart (0-10)',
			                         		default: 2
			                            },
                                        showLevelGridLines: {
                                           type: 'checkbox',
                                           label: 'Level dials gridlines',
                                           hover_tip: 'Display indicators for the middle of the SRS levels',
                                           default: false
                                        }
                                    }
                                },
                                srsTotalProgress_group: {
                                    type: 'group',
                                    label: 'Total Progress Bar',
                                    content: {
                                        showTotalProgress: {
                                            type: 'dropdown',
                                            label: 'Display Total Progress Bar',
                                            hover_tip: 'Total progress bar display style',
                                            default: '1',
                                            content: {
                                               0: 'none',
                                               1: 'Long bar (extend under level progress panel)',
                                               2: 'Short bar (center only)',
                                               3: 'Long bar (extend under review forecast)',
                                               4: 'Full bar (full width)',
                                               5: 'Top bar (center only)'
                                            }
                                        },
                                        reverseTotalProgress: {
                                           type: 'checkbox',
                                           label: 'Reverse order',
                                           hover_tip: 'Display locked->burn (true) or burn->locked (false)',
                                           default: false
                                        }
                                    }
                                },
                            }
                        },
                        panelPage: {
                            type: 'page',
                            label: 'Panels',
                            hover_tip: 'Show or Hide Panels',
                            content: {
                                showForecast: {
                                    type: 'checkbox',
                                    label: 'Review forecast',
                                    hover_tip: 'Display or hide this panel',
                                    default: true
                                },
                                showRecentUnlocks: {
                                    type: 'checkbox',
                                    label: 'New unlocks in the last 30 days',
                                    hover_tip: 'Display or hide this panel',
                                    default: false
                                },
                                showCriticalItems: {
                                    type: 'checkbox',
                                    label: 'Critical condition items',
                                    hover_tip: 'Display or hide this panel',
                                    default: true
                                },
                                showRecentBurns: {
                                    type: 'checkbox',
                                    label: 'Burned items in the last 30 days',
                                    hover_tip: 'Display or hide this panel',
                                    default: true
                                },
                                showForumTopics: {
                                    type: 'checkbox',
                                    label: 'Community banner',
                                    hover_tip: 'Display or hide this panel',
                                    default: true
                                },
                                showExtraStudy: getShowExtraStudySettingObject(),
                                showReviewSummary: {
                                    type: 'checkbox',
                                    label: 'Review Summary Tile',
                                    hover_tip: 'Display or hide this panel',
                                    default: true
                                }
                            }
                        }
                    }
                }
            }
        }

        var dialog = new wkof.Settings(config);
        dialog.open();
    }

    function getShowExtraStudySettingObject() {
        if (isESMPresent()) {
            return {
                type: 'html',
                label: 'Extra Study',
                hover_tip: 'Location of Extra Study panel',
                html: '<div class="right"><label style="text-align: left; margin: 5px; color: grey;">Governed by Extra Study Mover</label></div>',
                wrapper: 'row'
            }
        }
        else {
            return {
                type: 'dropdown',
                label: 'Extra Study',
                hover_tip: 'Location of Extra Study panel',
                default: '1',
                content: {
                    0: 'none',
                    1: 'Above level progress',
                    6: 'Below level progress',
                    5: 'Above SRS progress arc',
                    2: 'Below SRS progress arc',
                    3: 'Above review forecast',
                    4: 'Below review forecast'
                }
            }
        }
    }
})();