#!/usr/bin/env kjscmd
/***********************************************************************
    Copyright notice

    Copyright 2009 Franck Quélain <shift (at) free.fr>
    All rights reserved

    This file is part of KjsCAC.

    KjsCAC is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    KjsCAC is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with KjsCAC.  If not, see <http://www.gnu.org/licenses/>.
 ************************************************************************/

/* Constants for dichromat calculations */
var RGBtoLMS = new Array(new Array(0.05059983, 0.08585369, 0.00952420),
                        new Array(0.01893033, 0.08925308, 0.01370054),
                        new Array(0.00292202, 0.00975732, 0.07145979));
var LMStoRGB = new Array(new Array(30.830854, -29.832659, 1.610474),
                        new Array(-6.481468, 17.715578, -2.532642),
                        new Array(-0.375690, -1.199062, 14.273846));
var Gamma = new Array(2.1 , 2.0, 2.1);

var LE = 0.14597772;
var ME = 0.12188395;
var SE = 0.08413913;

var DICHROMAT_TYPE_NORMAL = "";
var DICHROMAT_TYPE_PROTANOPIA = "_prot";
var DICHROMAT_TYPE_DEUTERANOPIA = "_deut";
var DICHROMAT_TYPE_TRITERANOPIA = "_trit";
var DICHROMAT_TYPES = new Array();
DICHROMAT_TYPES[0]=DICHROMAT_TYPE_NORMAL;
DICHROMAT_TYPES[1]=DICHROMAT_TYPE_PROTANOPIA;
DICHROMAT_TYPES[2]=DICHROMAT_TYPE_DEUTERANOPIA;
DICHROMAT_TYPES[3]=DICHROMAT_TYPE_TRITERANOPIA;

var BRIGHTNESS_TRESHOLD = 125;
var COLOR_TRESHOLD = 500;

// Load GUI
var loader = new QUiLoader();
var win = loader.load("ressources/kjsCAC.ui");

var ExitAction = win.findChild("fileExitAction");
var AboutAction = win.findChild("helpAboutAction");
var ForegroundColorButton = win.findChild("foregroundColorButton");
var BackgroundColorButton = win.findChild("backgroundColorButton");

var BrightnessDiffEdit = new Array(4);
var ColorDiffEdit = new Array(4);
var ResultEdit = new Array(4);
var DemoLabel = new Array(4);
for(dichromatTypeKey in DICHROMAT_TYPES) {
    var dichromatType = DICHROMAT_TYPES[dichromatTypeKey];
    BrightnessDiffEdit[dichromatType] = win.findChild("brightnessDiffEdit" + dichromatType);
    ColorDiffEdit[dichromatType] = win.findChild("colorDiffEdit" + dichromatType);
    ResultEdit[dichromatType] = win.findChild("resultEdit" + dichromatType);
    DemoLabel[dichromatType] = win.findChild("demoLabel" + dichromatType);
}

// Connect action and widget
connect(ExitAction, 'activated()', win, 'close()');
connect(AboutAction, 'activated()', this, 'about()');
connect(ForegroundColorButton, 'changed(QColor)', this, 'setForegroundColor(QColor)');
connect(BackgroundColorButton, 'changed(QColor)', this, 'setBackgroundColor(QColor)');

// Initialise the colors
var foregroundColor = new QColor('black');
var backgroundColor = new QColor('white');
setForegroundColor(foregroundColor);
setBackgroundColor(backgroundColor);

// Show and execute the application
win.show();
exec();

/**
 * Display about dialog box
 */
function about() {
    alert("KjsCAC - Color Accessibility Checker\nVersion: 4.0\n\n© Franck Quélain (shift at free.fr)");
}

/**
 * Modification of the foreground color
 * @param color Color chosen
 */
function setForegroundColor(color) {
    foregroundColor = color;
    updateResult();
}

/**
 * Modification of the background color
 * @param color Color chosen
 */
function setBackgroundColor(color) {
    backgroundColor = color;
    updateResult();
}

/**
 * Update the result
 */
function updateResult(){

    // background color
    var br = backgroundColor.red();
    var bg = backgroundColor.green();
    var bb = backgroundColor.blue();

    // foreground color
    var fr = foregroundColor.red();
    var fg = foregroundColor.green();
    var fb = foregroundColor.blue();

    // ------ Normal
    updateResultForDichromatType(br, bg, bb, fr, fg, fb, DICHROMAT_TYPE_NORMAL);

    // ----- Protanopia
    updateResultForDichromatType(br, bg, bb, fr, fg, fb, DICHROMAT_TYPE_PROTANOPIA);

    // ----- Deuteranopia
    updateResultForDichromatType(br, bg, bb, fr, fg, fb, DICHROMAT_TYPE_DEUTERANOPIA);

    // ----- Triteranopia
    updateResultForDichromatType(br, bg, bb, fr, fg, fb, DICHROMAT_TYPE_TRITERANOPIA);
}

/**
 * Update result for dichromat type
 * @param br Background red
 * @param bg Background green
 * @param bb Background blue
 * @param fr Foreground red
 * @param fg Foreground green
 * @param fb Foreground blue
 * @param dichromatType Dichromat type
 */
function updateResultForDichromatType(br, bg, bb, fr, fg, fb, dichromatType) {

    var foregroundColorConverted = convertDichromatColors(fr, fg, fb, dichromatType);
    var foregroundColor=RGBToColor(foregroundColorConverted[0], foregroundColorConverted[1], foregroundColorConverted[2]);

    var backgroundColorConverted = convertDichromatColors(br, bg, bb, dichromatType);
    var backgroundColor=RGBToColor(backgroundColorConverted[0], backgroundColorConverted[1], backgroundColorConverted[2]);

    var css = "* {color: " + foregroundColor + "; background-color: " + backgroundColor + "; }";
    DemoLabel[dichromatType].setStyleSheet(css);

    var results = calculateDiff(backgroundColorConverted[0], backgroundColorConverted[1], backgroundColorConverted[2], foregroundColorConverted[0], foregroundColorConverted[1], foregroundColorConverted[2]);

    var brightnessDifference = results[0];
    BrightnessDiffEdit[dichromatType].setText(""+brightnessDifference);
    
    var colorDifference = results[1];
    ColorDiffEdit[dichromatType].setText(""+colorDifference);
  
    var RedStyle="* {color: red}";
    var BlueStyle="* {color: blue}";

    if ((brightnessDifference >= BRIGHTNESS_TRESHOLD) && (colorDifference >= COLOR_TRESHOLD))   {
        ResultEdit[dichromatType].setText("YES!"); // compliant
        ResultEdit[dichromatType].setStyleSheet(BlueStyle);
    }else if ((brightnessDifference >= BRIGHTNESS_TRESHOLD) || (colorDifference >= COLOR_TRESHOLD)){
        ResultEdit[dichromatType].setText("sort of..."); // sort of compliant
        ResultEdit[dichromatType].setStyleSheet(RedStyle);
    }else{
        ResultEdit[dichromatType].setText("NO!"); // not compliant "Poor visibility between text and background colors."
        ResultEdit[dichromatType].setStyleSheet(RedStyle);
    }
}

/** Calculate differences (color and brightness)
 * @param br Background red
 * @param bg Background green
 * @param bb Background blue
 * @param fr Foreground red
 * @param fg Foreground green
 * @param fb Foreground blue
 * @return Array(brightness difference, color difference)
 */
function calculateDiff(br, bg, bb, fr, fg, fb) {
    var bY=((br * 299) + (bg * 587) + (bb * 114)) / 1000;
    var fY=((fr * 299) + (fg * 587) + (fb * 114)) / 1000;
    var brightnessDifference = Math.abs(bY-fY);

    var colorDifference = (Math.max (fr, br) - Math.min (fr, br)) +
                          (Math.max (fg, bg) - Math.min (fg, bg)) +
                          (Math.max (fb, bb) - Math.min (fb, bb));

    return new Array(brightnessDifference, colorDifference);
}

/**
 * Convert RGB color to RGB color seen by a color blind person
 * @param red Red
 * @param green Green
 * @param blue, Blue
 * @param dichromatType Dichromat type
 * @return Array(r, g, b)
 */
function convertDichromatColors(red, green, blue, dichromatType) {

    var a1, b1, c1, a2, b2, c2, a, t;

    if(dichromatType == DICHROMAT_TYPE_NORMAL) {
        return new Array(red, green, blue);
    }

    red = Math.pow(red / 255, Gamma[0]);
    green = Math.pow(green / 255, Gamma[1]);
    blue = Math.pow(blue / 255, Gamma[2]);

    oldRed = red;
    oldGreen = green;

    red = oldRed * RGBtoLMS[0][0] + oldGreen * RGBtoLMS[0][1] + blue * RGBtoLMS[0][2];
    green = oldRed * RGBtoLMS[1][0] + oldGreen * RGBtoLMS[1][1] + blue * RGBtoLMS[1][2];
    blue = oldRed * RGBtoLMS[2][0] + oldGreen * RGBtoLMS[2][1] + blue * RGBtoLMS[2][2];

    switch(dichromatType) {
        case DICHROMAT_TYPE_PROTANOPIA :
        case DICHROMAT_TYPE_DEUTERANOPIA :
            //575nm
            a1 = -0.0614;
            b1 = 0.0833;
            c1 = -0.0141;
            //475nm
            a2 = 0.0584;
            b2 = -0.0794;
            c2 = 0.0136;

            break;
        case DICHROMAT_TYPE_TRITERANOPIA :
            //660nm
            a1 = 0;
            b1 = 0.0076;
            c1 = -0.011;
            //485nm
            a2 = 0.0254;
            b2 = -0.0416;
            c2 = 0.0163;

            break;
    }

    switch(dichromatType) {
        case DICHROMAT_TYPE_PROTANOPIA :

            if(green > 0) {
                a = blue / green;
            } else {
                a = 0;
            }

            t = SE / ME;

            if(a < t) {
                red = -(b1 * green + c1 * blue) / a1;
            } else {
                red = -(b2 * green + c2 * blue) / a2;
            }

            break;
        case DICHROMAT_TYPE_DEUTERANOPIA :

            if(red > 0) {
                a = blue / red;
            } else {
                a = 0;
            }

            t = SE/LE;

            if(a < t) {
                green = -(a1 * red + c1 * blue) / b1;
            } else {
                green = -(a2 * red + c2 * blue) / b2;
            }

            break;
        case DICHROMAT_TYPE_TRITERANOPIA :

            if(red > 0) {
                a = green / red;
            } else {
                a = 0;
            }

            t = ME/LE;

            if(a < t) {
                blue = -(a1 * red + b1 * green) / c1;
            } else {
                blue = -(a2 * red + b2 * green) / c2;
            }

            break;
    }

    oldRed = red;
    oldGreen = green;
    red = oldRed * LMStoRGB[0][0] + oldGreen * LMStoRGB[0][1] + blue * LMStoRGB[0][2];
    green = oldRed * LMStoRGB[1][0] + oldGreen * LMStoRGB[1][1] + blue * LMStoRGB[1][2];
    blue = oldRed * LMStoRGB[2][0] + oldGreen * LMStoRGB[2][1] + blue * LMStoRGB[2][2];

    if(red > 0) {
        red = 255 * Math.pow(red, 1 / Gamma[0]);
    } else {
        red = 0;
    }

    if(green > 0) {
        green = 255 * Math.pow(green, 1 / Gamma[1]);
    } else {
        green = 0;
    }

    if(blue > 0) {
        blue = 255 * Math.pow(blue, 1 / Gamma[2]);
    } else {
        blue = 0;
    }

    red = Math.floor(ensureRange(red, 0, 255));
    green = Math.floor(ensureRange(green, 0, 255));
    blue = Math.floor(ensureRange(blue, 0, 255));
    return new Array(red, green, blue);
}

/**
 * Ensure that a value is in a range
 * @param value Value to ensure range
 * @param min Minimum range
 * @param max Maximum range
 */
function ensureRange(value, min, max) {
    return Math.max(min, Math.min(value, max));
}

/**
 * Convert color in RGB
 *
 * @param Color #......
 * @return r,g,b array
 */
function colorToRGB(color) {
    var r = parseInt(color.substring(1,3),16);
    var g = parseInt(color.substring(3,5),16);
    var b = parseInt(color.substring(5,7),16);

    return new Array(r, g, b);
}

function RGBToColor(r, g, b) {
    return "#" + decToHex(r) + decToHex(g) + decToHex(b);
}

/**
 * Decimal to hexadecimal conversion.
 *
 * @param dec Decimal number between 0 and 255
 * @return Hexadecimal number in 2 characters
 */
function decToHex(dec) {
    var hexStr = "0123456789ABCDEF";
    var low = dec % 16;
    var high = (dec - low)/16;
    hex = "" + hexStr.charAt(high) + hexStr.charAt(low);
    return hex;
}
