<commit_before>/**
 * Configurable constants
 */

import { today } from "misc/date";

export const E_NO_STORAGE = "Your browser does not support HTML5 storage, so logins won't be remembered.";

export const MSG_TIME_DEBUG  = 0;
export const MSG_TIME_WARN   = 2000;
export const MSG_TIME_ERROR  = 5000;
export const MSG_TIME_FATAL  = 0;

export const MIN_MSG_LEVEL  = 1;

export const PIE_LABEL_RADIUS_START = 1.1;
export const PIE_LABEL_RADIUS_SCALE = 1.2;
export const PIE_LABEL_INSIDE_RADIUS = 0.6;
export const PIE_LABEL_SWITCH_POINT = 0.65;
export const PIE_LABEL_SCALE_FACTOR_PRE = 0.4;
export const PIE_LABEL_SCALE_FACTOR_POST = 1.2;
export const PIE_SMALL_LABEL_OFFSET = 10;
export const PIE_DEPTH = 10;

export const NAV_HANDLE_EVENT = "click";

export const SEARCH_SUGGESTION_THROTTLE_TIME = 250;

export const ANALYSIS_VIEW_WIDTH   = 500;
export const ANALYSIS_VIEW_HEIGHT  = 500;

export const STOCKS_GET_PRICES = true;
export const STOCKS_GRAPH_DETAIL = 250;
export const STOCKS_GRAPH_HEIGHT = 50;
export const STOCKS_SIDEBAR_WIDTH = 260;
export const STOCKS_REFRESH_INTERVAL = 6000;
export const STOCKS_HL_TIME = 7000;

export const GRAPH_FUND_ITEM_LINE_WIDTH = 1.5;
export const GRAPH_FUND_ITEM_TENSION = 0.85;
export const GRAPH_FUND_HISTORY_WIDTH = 500;
export const GRAPH_FUND_HISTORY_WIDTH_NARROW = 500;
export const GRAPH_FUND_HISTORY_TENSION = 0.65;
export const GRAPH_FUND_HISTORY_NUM_TICKS = 10;
export const GRAPH_FUND_HISTORY_LINE_WIDTH = 1.5;
export const GRAPH_FUND_HISTORY_POINT_RADIUS = 3;
export const GRAPH_FUND_HISTORY_MOVING_AVG = [30, 90];
export const GRAPH_FUND_HISTORY_MODE_PERCENT = 0;
export const GRAPH_FUND_HISTORY_MODE_ABSOLUTE = 1;
export const GRAPH_FUND_HISTORY_MODE_PRICE = 2;
export const GRAPH_FUND_HISTORY_PERIODS = [
  ["year", [1, 5]],
  ["month", [1, 3]]
];
export const GRAPH_FUND_HISTORY_DEFAULT_PERIOD = "year1";

export const GRAPH_BALANCE_NUM_TICKS = 5;

export const GRAPH_KEY_SIZE = 12;
export const GRAPH_KEY_OFFSET_X = 5;
export const GRAPH_KEY_OFFSET_Y = 34;

export const GRAPH_ZOOM_SPEED = 1 / 10;
export const GRAPH_ZOOM_MAX = 1 / 100;

export const AVERAGE_MEAN = 0;
export const AVERAGE_MEDIAN = 1;
export const AVERAGE_MEAN_GEOM = 2;

export const FUTURE_INVESTMENT_RATE = 0.1;

export const COLOR_GRAPH_TITLE = "#000";
export const COLOR_DARK = "#333";
export const COLOR_LIGHT = "#eee";
export const COLOR_LIGHT_GREY = "#999";

export const COLOR_PROFIT = "#0c3";
export const COLOR_LOSS = "#c30";
export const COLOR_PROFIT_LIGHT = "#f4fff4";
export const COLOR_LOSS_LIGHT = "#fff4f4";

export const COLOR_BALANCE_ACTUAL = "#039";
export const COLOR_BALANCE_PREDICTED = "#f00";
export const COLOR_BALANCE_STOCKS = "rgba(200, 200, 200, 0.5)";

export const COLOR_GRAPH_FUND_ITEM = "#4286f4";
export const COLOR_GRAPH_FUND_LINE = "#000";

export const COLOR_CATEGORY = {
  stocks:  [84, 110, 122],
  bills: [183, 28, 28],
  food: [67, 160, 71],
  general: [1, 87, 155],
  holiday: [0, 137, 123],
  social: [191, 158, 36],
  income: [36, 191, 55],
  out: [191, 36, 36],
  net: [[36, 191, 55], [191, 36, 36]],
  predicted: [36, 191, 55],
  balance: [36, 191, 55]
};

export const COLOR_PIE_L1 = "#f15854";
export const COLOR_PIE_L2 = "#decf3f";
export const COLOR_PIE_L3 = "#b276b2";
export const COLOR_PIE_M1 = "#b2912f";
export const COLOR_PIE_M2 = "#f17cb0";
export const COLOR_PIE_M3 = "#60bd68";
export const COLOR_PIE_S1 = "#faa43a";
export const COLOR_PIE_S2 = "#5da5da";

export const FONT_AXIS_LABEL = "11px Arial, Helvetica, sans-serif";
export const FONT_GRAPH_TITLE = "16px bold Arial, Helvetica, sans-serif";
export const FONT_GRAPH_TITLE_LARGE = "18px bold Arial, Helvetica, sans-serif";
export const FONT_GRAPH_KEY = "13px Arial, Helvetica, sans-serif";
export const FONT_GRAPH_KEY_SMALL = "11px Arial, Helvetica, sans-serif";

export const PAGE_DEF = {
  funds: {
    page: "funds",
    col: ["date", "item", "transactions", "cost", "price"],
    colShort: ["d", "i", "t", "c", "P"],
    colEdit: [0, 1, 2, 3],
    dataType: ["date", "text-nosug", "transactions", "cost"],
    addDefaultVal: {
      date: today.format(),
      item: "",
      transactions: [],
      cost: "0.00"
    },
    daily: false
  },
  income: {
    page: "income",
    col: ["date", "item", "cost"],
    colShort: ["d", "i", "c"],
    dataType: ["date", "text", "cost"],
    addDefaultVal:  {
      date: today.format(),
      item: "",
      cost: "0.00"
    },
    limit: true,
    daily: false,
    suggestions: false
  },
  bills: {
    page: "bills",
    col: ["date", "item", "cost"],
    colShort: ["d", "i", "c"],
    limit: true,
    dataType: ["date", "text", "cost"],
    addDefaultVal:  {
      date: today.format(),
      item: "",
      cost: "0.00"
    },
    daily: false,
    graphHidden: true
  },
  food: {
    page: "food",
    col: ["date", "item", "category", "cost", "shop"],
    colShort: ["d", "i", "k", "c", "s"],
    dataType: ["date", "text", "text", "cost", "text"],
    limit: true,
    addDefaultVal:  {
      date: today.format(),
      item: "",
      category: "",
      cost: "0.00",
      shop: ""
    },
    daily: true,
    drawPie: true
  },
  general: {
    page: "general",
    col: ["date", "item", "category", "cost", "shop"],
    colShort: ["d", "i", "k", "c", "s"],
    dataType: ["date", "text", "text", "cost", "text"],
    limit: true,
    addDefaultVal:  {
      date: today.format(),
      item: "",
      category: "",
      cost: "0.00",
      shop: ""
    },
    daily: true,
    drawPie: true
  },
  social: {
    page: "social",
    col: ["date", "item", "society", "cost", "shop"],
    colShort: ["d", "i", "y", "c", "s"],
    dataType: ["date", "text", "text", "cost", "text"],
    addDefaultVal:  {
      date: today.format(),
      item: "",
      society: "",
      cost: "0.00",
      shop: ""
    },
    daily: false,
    drawPie: true
  },
  holiday: {
    page: "holiday",
    col: ["date", "item", "holiday", "cost", "shop"],
    colShort: ["d", "i", "h", "c", "s"],
    dataType: ["date", "text", "text", "cost", "text"],
    addDefaultVal:  {
      date: today.format(),
      item: "",
      holiday: "",
      cost: "0.00",
      shop: ""
    },
    daily: false,
    drawPie: true
  }
};

export const STOCK_INDICES = {
  "LON:CTY": "CTY",
  "INDEXFTSE:UKX": "FTSE100",
  "INDEXFTSE:SMX": "FTSE Small",
  "INDEXSTOXX:SX5E": "ES50",
  "INDEXSP:SPE350": "S&PE350",
  "INDEXHANGSENG:HSI": "HangSeng",
  "INDEXTOPIX:TOPIX": "Tokyo"
};

<commit_msg>Updated stock indices<commit_after>/**
 * Configurable constants
 */

import { today } from "misc/date";

export const E_NO_STORAGE = "Your browser does not support HTML5 storage, so logins won't be remembered.";

export const MSG_TIME_DEBUG  = 0;
export const MSG_TIME_WARN   = 2000;
export const MSG_TIME_ERROR  = 5000;
export const MSG_TIME_FATAL  = 0;

export const MIN_MSG_LEVEL  = 1;

export const PIE_LABEL_RADIUS_START = 1.1;
export const PIE_LABEL_RADIUS_SCALE = 1.2;
export const PIE_LABEL_INSIDE_RADIUS = 0.6;
export const PIE_LABEL_SWITCH_POINT = 0.65;
export const PIE_LABEL_SCALE_FACTOR_PRE = 0.4;
export const PIE_LABEL_SCALE_FACTOR_POST = 1.2;
export const PIE_SMALL_LABEL_OFFSET = 10;
export const PIE_DEPTH = 10;

export const NAV_HANDLE_EVENT = "click";

export const SEARCH_SUGGESTION_THROTTLE_TIME = 250;

export const ANALYSIS_VIEW_WIDTH   = 500;
export const ANALYSIS_VIEW_HEIGHT  = 500;

export const STOCKS_GET_PRICES = true;
export const STOCKS_GRAPH_DETAIL = 250;
export const STOCKS_GRAPH_HEIGHT = 50;
export const STOCKS_SIDEBAR_WIDTH = 260;
export const STOCKS_REFRESH_INTERVAL = 6000;
export const STOCKS_HL_TIME = 7000;

export const GRAPH_FUND_ITEM_LINE_WIDTH = 1.5;
export const GRAPH_FUND_ITEM_TENSION = 0.85;
export const GRAPH_FUND_HISTORY_WIDTH = 500;
export const GRAPH_FUND_HISTORY_WIDTH_NARROW = 500;
export const GRAPH_FUND_HISTORY_TENSION = 0.65;
export const GRAPH_FUND_HISTORY_NUM_TICKS = 10;
export const GRAPH_FUND_HISTORY_LINE_WIDTH = 1.5;
export const GRAPH_FUND_HISTORY_POINT_RADIUS = 3;
export const GRAPH_FUND_HISTORY_MOVING_AVG = [30, 90];
export const GRAPH_FUND_HISTORY_MODE_PERCENT = 0;
export const GRAPH_FUND_HISTORY_MODE_ABSOLUTE = 1;
export const GRAPH_FUND_HISTORY_MODE_PRICE = 2;
export const GRAPH_FUND_HISTORY_PERIODS = [
  ["year", [1, 5]],
  ["month", [1, 3]]
];
export const GRAPH_FUND_HISTORY_DEFAULT_PERIOD = "year1";

export const GRAPH_BALANCE_NUM_TICKS = 5;

export const GRAPH_KEY_SIZE = 12;
export const GRAPH_KEY_OFFSET_X = 5;
export const GRAPH_KEY_OFFSET_Y = 34;

export const GRAPH_ZOOM_SPEED = 1 / 10;
export const GRAPH_ZOOM_MAX = 1 / 100;

export const AVERAGE_MEAN = 0;
export const AVERAGE_MEDIAN = 1;
export const AVERAGE_MEAN_GEOM = 2;

export const FUTURE_INVESTMENT_RATE = 0.1;

export const COLOR_GRAPH_TITLE = "#000";
export const COLOR_DARK = "#333";
export const COLOR_LIGHT = "#eee";
export const COLOR_LIGHT_GREY = "#999";

export const COLOR_PROFIT = "#0c3";
export const COLOR_LOSS = "#c30";
export const COLOR_PROFIT_LIGHT = "#f4fff4";
export const COLOR_LOSS_LIGHT = "#fff4f4";

export const COLOR_BALANCE_ACTUAL = "#039";
export const COLOR_BALANCE_PREDICTED = "#f00";
export const COLOR_BALANCE_STOCKS = "rgba(200, 200, 200, 0.5)";

export const COLOR_GRAPH_FUND_ITEM = "#4286f4";
export const COLOR_GRAPH_FUND_LINE = "#000";

export const COLOR_CATEGORY = {
  stocks:  [84, 110, 122],
  bills: [183, 28, 28],
  food: [67, 160, 71],
  general: [1, 87, 155],
  holiday: [0, 137, 123],
  social: [191, 158, 36],
  income: [36, 191, 55],
  out: [191, 36, 36],
  net: [[36, 191, 55], [191, 36, 36]],
  predicted: [36, 191, 55],
  balance: [36, 191, 55]
};

export const COLOR_PIE_L1 = "#f15854";
export const COLOR_PIE_L2 = "#decf3f";
export const COLOR_PIE_L3 = "#b276b2";
export const COLOR_PIE_M1 = "#b2912f";
export const COLOR_PIE_M2 = "#f17cb0";
export const COLOR_PIE_M3 = "#60bd68";
export const COLOR_PIE_S1 = "#faa43a";
export const COLOR_PIE_S2 = "#5da5da";

export const FONT_AXIS_LABEL = "11px Arial, Helvetica, sans-serif";
export const FONT_GRAPH_TITLE = "16px bold Arial, Helvetica, sans-serif";
export const FONT_GRAPH_TITLE_LARGE = "18px bold Arial, Helvetica, sans-serif";
export const FONT_GRAPH_KEY = "13px Arial, Helvetica, sans-serif";
export const FONT_GRAPH_KEY_SMALL = "11px Arial, Helvetica, sans-serif";

export const PAGE_DEF = {
  funds: {
    page: "funds",
    col: ["date", "item", "transactions", "cost", "price"],
    colShort: ["d", "i", "t", "c", "P"],
    colEdit: [0, 1, 2, 3],
    dataType: ["date", "text-nosug", "transactions", "cost"],
    addDefaultVal: {
      date: today.format(),
      item: "",
      transactions: [],
      cost: "0.00"
    },
    daily: false
  },
  income: {
    page: "income",
    col: ["date", "item", "cost"],
    colShort: ["d", "i", "c"],
    dataType: ["date", "text", "cost"],
    addDefaultVal:  {
      date: today.format(),
      item: "",
      cost: "0.00"
    },
    limit: true,
    daily: false,
    suggestions: false
  },
  bills: {
    page: "bills",
    col: ["date", "item", "cost"],
    colShort: ["d", "i", "c"],
    limit: true,
    dataType: ["date", "text", "cost"],
    addDefaultVal:  {
      date: today.format(),
      item: "",
      cost: "0.00"
    },
    daily: false,
    graphHidden: true
  },
  food: {
    page: "food",
    col: ["date", "item", "category", "cost", "shop"],
    colShort: ["d", "i", "k", "c", "s"],
    dataType: ["date", "text", "text", "cost", "text"],
    limit: true,
    addDefaultVal:  {
      date: today.format(),
      item: "",
      category: "",
      cost: "0.00",
      shop: ""
    },
    daily: true,
    drawPie: true
  },
  general: {
    page: "general",
    col: ["date", "item", "category", "cost", "shop"],
    colShort: ["d", "i", "k", "c", "s"],
    dataType: ["date", "text", "text", "cost", "text"],
    limit: true,
    addDefaultVal:  {
      date: today.format(),
      item: "",
      category: "",
      cost: "0.00",
      shop: ""
    },
    daily: true,
    drawPie: true
  },
  social: {
    page: "social",
    col: ["date", "item", "society", "cost", "shop"],
    colShort: ["d", "i", "y", "c", "s"],
    dataType: ["date", "text", "text", "cost", "text"],
    addDefaultVal:  {
      date: today.format(),
      item: "",
      society: "",
      cost: "0.00",
      shop: ""
    },
    daily: false,
    drawPie: true
  },
  holiday: {
    page: "holiday",
    col: ["date", "item", "holiday", "cost", "shop"],
    colShort: ["d", "i", "h", "c", "s"],
    dataType: ["date", "text", "text", "cost", "text"],
    addDefaultVal:  {
      date: today.format(),
      item: "",
      holiday: "",
      cost: "0.00",
      shop: ""
    },
    daily: false,
    drawPie: true
  }
};

export const STOCK_INDICES = {
  "LON:CTY": "CTY",
  "INDEXFTSE:UKX": "FTSE 100",
  "INDEXSP:.INX": "S&P 500",
  "INDEXDJX:.DJI": "DJIA",
  "INDEXNASDAQ:NDX": "NASDAQ 100",
  "INDEXNIKKEI:NI225": "Nikkei 225"
};

<|endoftext|>


<commit_before><commit_msg>Update ogario.v4.js<commit_after><|endoftext|>


<commit_before>/**
 * Client-side image resizer module
 */

/*global require,define,window,console,atob,unescape,ArrayBuffer,Uint8Array*/

define("krusovice/tools/resizer", ["krusovice/thirdparty/jquery",
    "krusovice/thirdparty/jpegmeta"], function($, JpegMeta) {

    "use strict";

    //
    // private functions
    //

    /**
     * http://stackoverflow.com/questions/4998908/convert-data-uri-to-file-then-append-to-formdata/5100158
     *
     *
     */
    function dataURItoBlob(dataURI, callback) {
        // convert base64 to raw binary data held in a string
        // doesn't handle URLEncoded DataURIs

        var byteString;
        if (dataURI.split(',')[0].indexOf('base64') >= 0) {
            byteString = atob(dataURI.split(',')[1]);
        } else {
            byteString = unescape(dataURI.split(',')[1]);
        }

        // separate out the mime component
        var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];

        // write the bytes of the string to an ArrayBuffer
        var ab = new ArrayBuffer(byteString.length);
        var ia = new Uint8Array(ab);
        for (var i = 0; i < byteString.length; i++) {
            ia[i] = byteString.charCodeAt(i);
        }

        // write the ArrayBuffer to a blob, and you're done
        var BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder;
        var bb = new BlobBuilder();
        bb.append(ab);
        return bb.getBlob(mimeString);
    }

    function getAsJPEGBlob(canvas) {
        if(canvas.mozGetAsFile) {
            return canvas.mozGetAsFile("foo.jpg", "image/jpeg");
        } else {
            var data = canvas.toDataURL('image/jpeg', 0.7);
            var blob = dataURItoBlob(data);
            return blob;
        }
    }

    function saveCanvasAsFile(canvas) {
        var blob = getAsJPEGBlob(canvas);
        return blob;
    }

    function calculateAspectRatioFit(srcWidth, srcHeight, maxWidth, maxHeight) {

        var ratio = [maxWidth / srcWidth, maxHeight / srcHeight ];
        ratio = Math.min(ratio[0], ratio[1]);

        return { width:srcWidth*ratio, height:srcHeight*ratio };
     }

    /**
     * http://benno.id.au/blog/2009/12/30/html5-fileapi-jpegmeta
     */
    function extractJPEGData(data, name) {
        var jpeg = new JpegMeta.JpegFile(data, name);
        console.log(jpeg);

        var orientation = -1;
        if(jpeg.tiff) {
            if(jpeg.tiff.Orientation) {
                orientation =  jpeg.tiff.Orientation.value;
            }
        }

        console.log("Orientation:" + orientation);

        return jpeg;
    }

    /**
     * Calculate degrees and flip for the JPEG TIFF orientation to straighten it.
     *
     * http://www.impulseadventure.com/photo/exif-orientation.html
     *
     * @reurn {Number} Degrees to rotate the image to counter-clockwise to fix the roratino.
     */
    function calculateRotation(orientation) {

        // Got values 1,6,8,3 out of Galaxy S

        if(orientation == 8) {
            return 90;
        } else if(orientation == 3) {
            return 180;
        } else if(orientation == 6) {
            return 270;
        }
        return 0;
    }

    /**
     * Global stats of all processed images
     */
    var stats = {

        bytesIn : 0,

        bytesOut : 0,

        startedCount : 0,

        finishedCount : 0

    };

    /**
     * Change <input file> control to include a resized file instead of orignal one.
     *
     * http://davidflanagan.com/Talks/jsconf11/BytesAndBlobs.html
     */
    function updatePayload(files, resizer) {
        if(resizer.resized) {
            console.log(files);
        }
    }

    /**
     * Client-size image resizer done using HTML5.
     *
     * Batch resize multiple images into target parameters.
     * Asynchronously report and push the results to the caller.
     *
     *
     * Create Resizer object, give success and failure callbacks for it and call process(file).
     * The success() callback is called as success(resizer) and having access to all related data
     * in Resizer object.
     */
    function Resizer(cfg) {
        $.extend(this, cfg);
    }

    Resizer.prototype = {

        maximumWidth : 1024,

        maximumHeight : 768,

        /**
         * @param {Function} success Callback success(blob, metadata) for asyncrhonous processing
         *
         * @param {Function} failer Callback failure(msg) for asyncrhonous processing
         */
        success : null,

        failure : null,

        /**
         * Did we need to resize the file
         *
         * E.g. it was small enough in the begin with
         */
        resized : false,

        /**
         * Source local File object
         */
        file : null,

        /**
         * Extracted JPEG metadata
         */
        metadata : null,

        /**
         * Image object for the source image
         */
        image : null,

        /**
         * Contains resized image
         */
        canvas : null,

        /**
         * Resulting File object. Can be same as input file if no resize process took place.
         */
        blob : null,

        /**
         * Extract image file from canvas
         */
        processExtractImageFile : function() {
            this.blob = saveCanvasAsFile(this.canvas);

            stats.bytesOut += this.blob.size;

            stats.finishedCount += 1;

            this.resized = true;

            this.success(this);
        },

        /**
         * @param metadata: JPEG metadata
         */
        processCanvasResize : function() {
            var img = this.image;

            // No need to resize the image - it is small enough
            if(img.naturalWidth <= this.maximumWidth && img.naturalHeigth <= this.maximumHeight) {
                return this.processNoResize();
            }

            var rotatedSize = { width : img.naturalWidth, height : img.naturalHeight };

            // Rotate width / height to match orientation

            // degrees how much image must rotated to get it to the proper angle

            var rotation = 0;

            // XXX: Not supported, very rare use case
            // -1 to flip the image
            var mirror = 1;

            var orientation = this.getOrientation();
            if(orientation !== null) {
                rotation = calculateRotation(orientation);
            }

            if(rotation !== 0) {
                if(rotation == 90 || rotation == 270) {
                    rotatedSize = { width : img.naturalHeight, height : img.naturalWidth };
                }
            }

            console.log("Image rotation: " + rotation + " rotated size:" + rotatedSize.width + " x " + rotatedSize.height);

            var size = calculateAspectRatioFit(rotatedSize.width, rotatedSize.height, this.maximumWidth, this.maximumHeight);
            var canvas = document.createElement("canvas");
            canvas.width = size.width;
            canvas.height = size.height;
            var context = canvas.getContext("2d");

            //context.translate(canvas.width, 0);
            context.rotate(-Math.PI*2*rotation/360);

            var x=0, y=0, w=canvas.width, h=canvas.height;

            // Reset orientation parameters to match canvas rotation

            if(rotation == 90) {
                x = -canvas.height;
                y = 0;
                w = canvas.height;
                h = canvas.width;
            }

            if(rotation == 270) {
                x = 0;
                y = -canvas.width;
                w = canvas.height;
                h = canvas.width;
            }

            if(rotation == 180) {
                x = -canvas.width;
                y = -canvas.height;
            }

            console.log("drawImage():" + x + " " + y + " " + w + " " + h);

            // http://stackoverflow.com/questions/4649836/using-html5-canvas-rotate-image-about-arbitrary-point/4650102#4650102
            context.drawImage(img, x, y, w, h);

            this.canvas = canvas;
            this.processExtractImageFile();
        },

        /**
         * Create <img> from local file object and then process it forward for resize.
         */
        processSourceImage : function() {

            var img = document.createElement("img");
            var self = this;

            img.onload = function(e) {
                // Don't leak file data unneeded
                self.image = img;

                // Give UI thread some time to breathe
                setTimeout($.proxy(self.processCanvasResize, self), 10);
            };

            var reader = new window.FileReader();

            reader.onload = function(e) {
                img.src = e.target.result;
            };

            reader.onerror = function(e) {
                self.failure(self, e);
            };

            reader.readAsDataURL(this.file);
        },

        /**
         * Check if we have APIs needed to perform the resize
         */
        isResizeSupported : function() {

            var BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder;

            if(window.WebKitBlobBuilder && window.location.href.indexOf("file://") === 0) {
                // throw "Chrome et. al. do not support File API on file:// origin HTML files"
                return false;
            }

            return !!BlobBuilder;
        },

        /**
         * Fallback when we cannot resize on the client side
         */
        processNoResize : function() {
            this.finishedCount++;
            this.blob = this.file;
            this.resized = false;
            this.success(this);
        },

        read : function() {

            var reader = new window.FileReader();
            var self = this;

            reader.onload = function(event) {
                var data = event.target.result;

                stats.bytesIn += data.length;

                self.metadata = extractJPEGData(data, self.file.fileName);

                self.processSourceImage();
            };

            reader.onerror = function(event) {
                console.error("Could not read");
                console.error(event);
                console.error(self.file);
                self.failure(this, "File reading failed");
            };

            console.log("Starting reading");
            console.log(this.file);
            reader.readAsBinaryString(this.file);

        },

        /**
         * Resize file on the client side and notify about the results asynchronously
         */
        process : function(f) {

            this.file = f;

            stats.startedCount++;

            if(!this.isResizeSupported()) {
                this.processNoResize();
            }

            this.read();

        },

        /**
         * Get orientation from JPEG metadata or null if not available
         */
        getOrientation : function() {

            var jpeg = this.metadata;

            if(jpeg) {
                if(jpeg.tiff) {
                    if(jpeg.tiff.Orientation) {
                        return jpeg.tiff.Orientation.value;

                    }
                }
            }

            return null;
        }

    };

    //
    // Module API
    //

    return {
        Resizer : Resizer,
        stats : stats,
        updatePayload : updatePayload
    };

});

<commit_msg>Correctly handle Safari 5.1<commit_after>/**
 * Client-side image resizer module
 */

/*global require,define,window,console,atob,unescape,ArrayBuffer,Uint8Array*/

define("krusovice/tools/resizer", ["krusovice/thirdparty/jquery",
    "krusovice/thirdparty/jpegmeta"], function($, JpegMeta) {

    "use strict";

    //
    // private functions
    //

    /**
     * http://stackoverflow.com/questions/4998908/convert-data-uri-to-file-then-append-to-formdata/5100158
     *
     *
     */
    function dataURItoBlob(dataURI, callback) {
        // convert base64 to raw binary data held in a string
        // doesn't handle URLEncoded DataURIs

        var byteString;
        if (dataURI.split(',')[0].indexOf('base64') >= 0) {
            byteString = atob(dataURI.split(',')[1]);
        } else {
            byteString = unescape(dataURI.split(',')[1]);
        }

        // separate out the mime component
        var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];

        // write the bytes of the string to an ArrayBuffer
        var ab = new ArrayBuffer(byteString.length);
        var ia = new Uint8Array(ab);
        for (var i = 0; i < byteString.length; i++) {
            ia[i] = byteString.charCodeAt(i);
        }

        // write the ArrayBuffer to a blob, and you're done
        var BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder;
        var bb = new BlobBuilder();
        bb.append(ab);
        return bb.getBlob(mimeString);
    }

    function getAsJPEGBlob(canvas) {
        if(canvas.mozGetAsFile) {
            return canvas.mozGetAsFile("foo.jpg", "image/jpeg");
        } else {
            var data = canvas.toDataURL('image/jpeg', 0.7);
            var blob = dataURItoBlob(data);
            return blob;
        }
    }

    function saveCanvasAsFile(canvas) {
        var blob = getAsJPEGBlob(canvas);
        return blob;
    }

    function calculateAspectRatioFit(srcWidth, srcHeight, maxWidth, maxHeight) {

        var ratio = [maxWidth / srcWidth, maxHeight / srcHeight ];
        ratio = Math.min(ratio[0], ratio[1]);

        return { width:srcWidth*ratio, height:srcHeight*ratio };
     }

    /**
     * http://benno.id.au/blog/2009/12/30/html5-fileapi-jpegmeta
     */
    function extractJPEGData(data, name) {
        var jpeg = new JpegMeta.JpegFile(data, name);
        console.log(jpeg);

        var orientation = -1;
        if(jpeg.tiff) {
            if(jpeg.tiff.Orientation) {
                orientation =  jpeg.tiff.Orientation.value;
            }
        }

        console.log("Orientation:" + orientation);

        return jpeg;
    }

    /**
     * Calculate degrees and flip for the JPEG TIFF orientation to straighten it.
     *
     * http://www.impulseadventure.com/photo/exif-orientation.html
     *
     * @reurn {Number} Degrees to rotate the image to counter-clockwise to fix the roratino.
     */
    function calculateRotation(orientation) {

        // Got values 1,6,8,3 out of Galaxy S

        if(orientation == 8) {
            return 90;
        } else if(orientation == 3) {
            return 180;
        } else if(orientation == 6) {
            return 270;
        }
        return 0;
    }

    /**
     * Global stats of all processed images
     */
    var stats = {

        bytesIn : 0,

        bytesOut : 0,

        startedCount : 0,

        finishedCount : 0

    };

    /**
     * Change <input file> control to include a resized file instead of orignal one.
     *
     * http://davidflanagan.com/Talks/jsconf11/BytesAndBlobs.html
     */
    function updatePayload(files, resizer) {
        if(resizer.resized) {
            console.log(files);
        }
    }

    /**
     * Client-size image resizer done using HTML5.
     *
     * Batch resize multiple images into target parameters.
     * Asynchronously report and push the results to the caller.
     *
     *
     * Create Resizer object, give success and failure callbacks for it and call process(file).
     * The success() callback is called as success(resizer) and having access to all related data
     * in Resizer object.
     */
    function Resizer(cfg) {
        $.extend(this, cfg);
    }

    Resizer.prototype = {

        maximumWidth : 1024,

        maximumHeight : 768,

        /**
         * @param {Function} success Callback success(blob, metadata) for asyncrhonous processing
         *
         * @param {Function} failer Callback failure(msg) for asyncrhonous processing
         */
        success : null,

        failure : null,

        /**
         * Did we need to resize the file
         *
         * E.g. it was small enough in the begin with
         */
        resized : false,

        /**
         * Source local File object
         */
        file : null,

        /**
         * Extracted JPEG metadata
         */
        metadata : null,

        /**
         * Image object for the source image
         */
        image : null,

        /**
         * Contains resized image
         */
        canvas : null,

        /**
         * Resulting File object. Can be same as input file if no resize process took place.
         */
        blob : null,

        /**
         * Extract image file from canvas
         */
        processExtractImageFile : function() {
            this.blob = saveCanvasAsFile(this.canvas);

            stats.bytesOut += this.blob.size;

            stats.finishedCount += 1;

            this.resized = true;

            this.success(this);
        },

        /**
         * @param metadata: JPEG metadata
         */
        processCanvasResize : function() {
            var img = this.image;

            // No need to resize the image - it is small enough
            if(img.naturalWidth <= this.maximumWidth && img.naturalHeigth <= this.maximumHeight) {
                return this.processNoResize();
            }

            var rotatedSize = { width : img.naturalWidth, height : img.naturalHeight };

            // Rotate width / height to match orientation

            // degrees how much image must rotated to get it to the proper angle

            var rotation = 0;

            // XXX: Not supported, very rare use case
            // -1 to flip the image
            var mirror = 1;

            var orientation = this.getOrientation();
            if(orientation !== null) {
                rotation = calculateRotation(orientation);
            }

            if(rotation !== 0) {
                if(rotation == 90 || rotation == 270) {
                    rotatedSize = { width : img.naturalHeight, height : img.naturalWidth };
                }
            }

            console.log("Image rotation: " + rotation + " rotated size:" + rotatedSize.width + " x " + rotatedSize.height);

            var size = calculateAspectRatioFit(rotatedSize.width, rotatedSize.height, this.maximumWidth, this.maximumHeight);
            var canvas = document.createElement("canvas");
            canvas.width = size.width;
            canvas.height = size.height;
            var context = canvas.getContext("2d");

            //context.translate(canvas.width, 0);
            context.rotate(-Math.PI*2*rotation/360);

            var x=0, y=0, w=canvas.width, h=canvas.height;

            // Reset orientation parameters to match canvas rotation

            if(rotation == 90) {
                x = -canvas.height;
                y = 0;
                w = canvas.height;
                h = canvas.width;
            }

            if(rotation == 270) {
                x = 0;
                y = -canvas.width;
                w = canvas.height;
                h = canvas.width;
            }

            if(rotation == 180) {
                x = -canvas.width;
                y = -canvas.height;
            }

            console.log("drawImage():" + x + " " + y + " " + w + " " + h);

            // http://stackoverflow.com/questions/4649836/using-html5-canvas-rotate-image-about-arbitrary-point/4650102#4650102
            context.drawImage(img, x, y, w, h);

            this.canvas = canvas;
            this.processExtractImageFile();
        },

        /**
         * Create <img> from local file object and then process it forward for resize.
         */
        processSourceImage : function() {

            var img = document.createElement("img");
            var self = this;

            img.onload = function(e) {
                // Don't leak file data unneeded
                self.image = img;

                // Give UI thread some time to breathe
                setTimeout($.proxy(self.processCanvasResize, self), 10);
            };

            var reader = new window.FileReader();

            reader.onload = function(e) {
                img.src = e.target.result;
            };

            reader.onerror = function(e) {
                self.failure(self, e);
            };

            reader.readAsDataURL(this.file);
        },

        /**
         * Check if we have APIs needed to perform the resize
         */
        isResizeSupported : function() {

            var BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder;

            if(window.WebKitBlobBuilder && window.location.href.indexOf("file://") === 0) {
                // throw "Chrome et. al. do not support File API on file:// origin HTML files"
                return false;
            }

            // FileReader is available only in Safari nightly builds currently
            // https://developer.mozilla.org/en/DOM/FileReader

            if(!window.FileReader) {
                return false;
            }

            return !!BlobBuilder;
        },

        /**
         * Fallback when we cannot resize on the client side
         */
        processNoResize : function() {
            this.finishedCount++;
            this.blob = this.file;
            this.resized = false;
            this.success(this);
        },

        read : function() {

            var reader = new window.FileReader();
            var self = this;

            reader.onload = function(event) {
                var data = event.target.result;

                stats.bytesIn += data.length;

                self.metadata = extractJPEGData(data, self.file.fileName);

                self.processSourceImage();
            };

            reader.onerror = function(event) {
                console.error("Could not read");
                console.error(event);
                console.error(self.file);
                self.failure(this, "File reading failed");
            };

            console.log("Starting reading");
            console.log(this.file);
            reader.readAsBinaryString(this.file);

        },

        /**
         * Resize file on the client side and notify about the results asynchronously
         */
        process : function(f) {

            this.file = f;

            stats.startedCount++;

            if(!this.isResizeSupported()) {
                this.processNoResize();
            } else {
                // Start reading image off-the-disk
                this.read();
            }

        },

        /**
         * Get orientation from JPEG metadata or null if not available
         */
        getOrientation : function() {

            var jpeg = this.metadata;

            if(jpeg) {
                if(jpeg.tiff) {
                    if(jpeg.tiff.Orientation) {
                        return jpeg.tiff.Orientation.value;

                    }
                }
            }

            return null;
        }

    };

    //
    // Module API
    //

    return {
        Resizer : Resizer,
        stats : stats,
        updatePayload : updatePayload
    };

});

<|endoftext|>


<commit_before>/**
 * Copyright 2015 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

 /**
  * @fileOverview The ajax-based routing for IOWA subpages.
  */

IOWA.Router = (function() {

  "use strict";

  var MASTHEAD_BG_CLASS_REGEX = /(\s|^)bg-[a-z-]+(\s|$)/;

  /**
   * Tells what kind of transition is currently happening on the page,
   * e.g. 'hero-card-transition' or 'masthead-ripple-transition'.
   * @type {=string?}
   */
  var currentPageTransition = null;

  /**
   * True if the URL update was due to a user click. Used to differentiate a
   * popstate event from a link click and a history pop.
   * @type {boolean}
   */
  var navigationFromLinkClick = false;

  /**
   * Navigates to a new page via a hero card takeover transition.
   * @param {Event} e Event that triggered navigation.
   * @param {Element} el Element clicked.
   * @param {string} rippleColor Color of the ripple on the card.
   * @private
   */
  function playHeroTransition(e, el, rippleColor) {
    // TODO: This may need some perf tweaking for FF.
    var card = null;
    var currentEl = el;
    while (!card) {
      currentEl = currentEl.parentNode;
      if (currentEl.classList.contains('card__container')) {
        card = currentEl;
      }
    }
    IOWA.PageAnimation.play(
      IOWA.PageAnimation.pageCardTakeoverOut(
          card, e.pageX, e.pageY, 300, rippleColor),
      function() {
        IOWA.History.pushState({'path': el.pathname}, '', el.href);
      }
    );
  }

  /**
   * Navigates to a new page via a masthead nav item ripple transition.
   * @param {Event} e Event that triggered navigation.
   * @param {Element} el Element clicked.
   * @param {string} mastheadColor Color of the masthead.
   * @param {string} rippleColor Color of the ripple.
   * @param {boolean} isFadeRipple If true, ripple will just glimpse and fade.
   * @private
   */
  function playMastheadRippleTransition(
      e, el, mastheadColor, rippleColor, isFadeRipple) {

    var x = e.touches ? e.touches[0].pageX : e.pageX;
    var y = e.touches ? e.touches[0].pageY : e.pageY;
    var duration = isFadeRipple ? 300 : 600;
    var rippleAnim = IOWA.PageAnimation.ripple(
          IOWA.Elements.Ripple, x, y, duration,
          rippleColor, isFadeRipple);
    var animGroup = [
      rippleAnim,
      IOWA.PageAnimation.contentSlideOut(),
    ];
    if (!isFadeRipple) {
      var mastheadAnim = new Animation(IOWA.Elements.Masthead, [
          {backgroundColor: mastheadColor},
          {backgroundColor: rippleColor}
        ], {
          duration: 300,
          delay: 0,
          fill: 'forwards'  // Makes ripple keep its state after animation.
      });
      animGroup.push(mastheadAnim);
    }
    var animation = new AnimationGroup(animGroup);
    IOWA.PageAnimation.play(animation, function() {
      IOWA.History.pushState({'path': el.pathname}, '', el.href);
    });
  }

  /**
   * Navigates to a new page via ajax and page transitions.
   * @param {Event} e Event that triggered navigation.
   * @param {Element} el Element clicked.
   * @param {String} currentPage Current page id.
   * @param {String} nextPage Id of the page the user navigates to.
   * @private
   */
  function handleAjaxLink(e, el, currentPage, nextPage) {
    e.preventDefault();
    e.stopPropagation();

    var template = IOWA.Elements.Template;
    var bgClass = template.pages[nextPage] &&
                  template.pages[nextPage].mastheadBgClass;
    var prevBgClass = template.pages[currentPage].mastheadBgClass;

    template.navBgClass = bgClass;

    var isFadeRipple = prevBgClass === bgClass;
    var mastheadColor = template.rippleColors[prevBgClass];
    var rippleColor = isFadeRipple ? '#fff' : template.rippleColors[bgClass];

    if (currentPage !== nextPage) {
      IOWA.Elements.Template.fire('page-transition-start');
      if (el.hasAttribute('data-anim-ripple')) {
        currentPageTransition = 'masthead-ripple-transition';
        playMastheadRippleTransition(
            e, el, mastheadColor, rippleColor, isFadeRipple);
      } else if (el.hasAttribute('data-anim-drawer'))  {
        var handler = function(e) {
          e.target.removeEventListener('core-transitionend', handler);
          currentPageTransition = '';
          IOWA.History.pushState({'path': el.pathname}, '', el.href);
        };
        el.parentNode.addEventListener('core-transitionend', handler);
      } else if (el.hasAttribute('data-anim-card'))  {
        currentPageTransition = 'hero-card-transition';
        playHeroTransition(e, el, rippleColor);
      } else {
        currentPageTransition = '';
        IOWA.History.pushState({'path': el.pathname}, '', el.href);
      }
    }
    // TODO: Update meta.
  }

  /**
   * Navigates to a new page. Uses ajax for data-ajax-link links.
   * @param {Event} e Event that triggered navigation.
   * @private
   */
  function navigate(e) {
    console.log(e)

    // Allow user to open page in a new tab.
    if (e.metaKey || e.ctrlKey) {
      return;
    }

    navigationFromLinkClick = true;

    // Inject page if <a> has the data-ajax-link attribute.
    for (var i = 0; i < e.path.length; ++i) {
      var el = e.path[i];
      if (el.localName === 'a') {
        var currentPage = parsePageNameFromAbsolutePath(location.pathname);
        var nextPage = parsePageNameFromAbsolutePath(el.pathname);

        // First, record click event if link requests it.
        if (el.hasAttribute('data-track-link')) {
          IOWA.Analytics.trackEvent(
              'link', 'click', el.getAttribute('data-track-link'));
        }

        // Ignore links that go offsite.
        if (el.target) {
          return;
        }

        // Prevent navigations to the same page.
        // Note, this prevents in-page anchors. Use IOWA.Util.smoothScroll.
        if (currentPage === nextPage) {
          e.preventDefault();
          return;
        }

        // Do ajax page navigation if link requests it.
        if (el.hasAttribute('data-ajax-link')) {
          runPageHandler('unload', currentPage);
          handleAjaxLink(e, el, currentPage, nextPage);
        } else {
          currentPageTransition = 'no-transition';
        }

        return; // found first navigation element, quit here.
      }
    }
  }

  /**
   * Renders a new page by fetching partials through ajax.
   * @param {string} pageName The name of the new page.
   * @private
   */
  function renderPage(pageName) {
    var importURL = pageName + '?partial';

    // TODO(ericbidelman): update call when github.com/Polymer/polymer/pull/1128 lands.
    Polymer.import([importURL], function() {
      // Don't proceed if import didn't load correctly.
      var htmlImport = document.querySelector(
          'link[rel="import"][href="' + importURL + '"]');
      if (htmlImport && !htmlImport.import) {
        return;
      }

      // FF doesn't execute the <script> inside the main content <template>
      // (inside page partial import). Instead, the first time the partial is
      // loaded, find any script tags in and make them runnable by appending them back to the template.
      if (IOWA.Util.isFF() || IOWA.Util.isIE()) {
        var contentTemplate = document.querySelector(
           '#template-' + pageName + '-content');
        if (!contentTemplate) {
          var containerTemplate = htmlImport.import.querySelector(
              '[data-ajax-target-template="template-content-container"]');
          var scripts = containerTemplate.content.querySelectorAll('script');
          Array.prototype.forEach.call(scripts, function(node, i) {
            replaceScriptTagWithRunnableScript(node);
          });
        }
      }

      // Update content of the page.
      injectPageContent(pageName, htmlImport.import);
    });
  }

  /**
   * Replaces in-page <script> tag in xhr'd body content with runnable script.
   *
   * @param {Node} node Container element to replace script content.
   * @private
   */
  function replaceScriptTagWithRunnableScript(node) {
    var script = document.createElement('script');
    script.text = node.text || node.textContent || node.innerHTML;

    // IE doesn't execute the script when it's appended to the middle
    // of the DOM. Append it to body instead, then remove.
    if (IOWA.Util.isIE()) {
      document.body.appendChild(script);
      document.body.removeChild(script);
    } else {
      node.parentNode.replaceChild(script, node); // FF
    }
  }

  /**
   * Replaces templated content.
   * @private
   */
  function replaceTemplateContent(currentPageTemplates) {
    for (var j = 0; j < currentPageTemplates.length; j++) {
      var template = currentPageTemplates[j];
      var templateToReplace = document.getElementById(
          template.getAttribute('data-ajax-target-template'));
      if (templateToReplace) {
        templateToReplace.setAttribute('ref', template.id);
      }
    }
  }

  /**
   * Updates the page elements during the page transition.
   * @param {string} pageName New page identifier.
   * @param {NodeList} currentPageTemplates Content templates to be rendered.
   * @private
   */
  function updatePageElements(pageName, currentPageTemplates) {
    replaceTemplateContent(currentPageTemplates);

    var template = IOWA.Elements.Template;

    // Update menu/drawer selected item.
    template.selectedPage = pageName;
    IOWA.Elements.DrawerMenu.selected = pageName;

    var currentPageMeta = template.pages[pageName];
    document.body.id = 'page-' + pageName;
    document.title = currentPageMeta.title || 'Google I/O 2015';

    var previousPageMeta = template.pages[template.selectedPage];
    var previousMastheadColor = template.rippleColors[previousPageMeta.mastheadBgClass];
    var currentMastheadColor = template.rippleColors[currentPageMeta.mastheadBgClass];

    // Prepare the page for a smooth masthead transition.
    var mastheadBgClass = currentPageMeta.mastheadBgClass;
    template.navBgClass = mastheadBgClass;
    // This cannot be updated via data binding, because the masthead
    // is visible before the binding happens.
    IOWA.Elements.Masthead.className = IOWA.Elements.Masthead.className.replace(
        MASTHEAD_BG_CLASS_REGEX, ' ' + mastheadBgClass + ' ');

    // Transition masthead color.
    IOWA.PageAnimation.play(new Animation(IOWA.Elements.Masthead, [
      {backgroundColor: previousMastheadColor},
      {backgroundColor: currentMastheadColor}
    ], {
      duration: currentPageTransition ? 0 : 300,
      fill: 'forwards'
    }));

    // Hide the masthead ripple before proceeding with page transition.
    IOWA.PageAnimation.play(
        IOWA.PageAnimation.elementFadeOut(IOWA.Elements.Ripple, {duration: 0}));

    // Scroll to top of new page.
    IOWA.Elements.ScrollContainer.scrollTop = 0;

    // Wait 1 rAF for DOM to settle.
    IOWA.Elements.Template.async(function() {
      var animationFunc;
      if (currentPageTransition === 'hero-card-transition') {
        animationFunc = IOWA.PageAnimation.pageCardTakeoverIn;
      } else {
        animationFunc = IOWA.PageAnimation.pageSlideIn;
      }

      IOWA.PageAnimation.play(animationFunc(), function() {
        // Fire event when the page transitions are final.
        IOWA.Elements.Template.fire('page-transition-done');
      });

      currentPageTransition = '';
    });
  }

  /**
   * Runs animated page transition.
   * @param {string} pageName New page identifier.
   * @private
   */
  function animatePageIn(pageName) {
    // Prequery for content templates.
    var currentPageTemplates = document.querySelectorAll(
        '.js-ajax-' + pageName);
    if (!currentPageTransition) {
      var animation = IOWA.PageAnimation.contentSlideOut();
      IOWA.PageAnimation.play(animation, updatePageElements.bind(
          null, pageName, currentPageTemplates));
    } else if (currentPageTransition !== 'no-transition') {
      updatePageElements(pageName, currentPageTemplates);
    }
  }

  /**
   * Parses the page name out of the last entry in absolutePath, split on '/'.
   * Defaults to 'home' if absolutePath ends in '/' or is ''.
   * @private
   */
  function parsePageNameFromAbsolutePath(absolutePath) {
    return absolutePath.split('/').pop() || 'home';
  }

  /**
   * Renders a new page for the current location.
   * @private
   */
  function renderCurrentPage() {
    renderPage(parsePageNameFromAbsolutePath(window.location.pathname));
  }

  /**
   * Injects new page content into existing layout.
   * @param {string} pageName New page identifier.
   * @param {DocumentFragment} importContent HTML containing templates to be
   *    injected.
   * @private
   */
  function injectPageContent(pageName, importContent) {

    runPageHandler('load', pageName);

    // Add freshly fetched templates to DOM, if not yet present.
    var newTemplates = importContent.querySelectorAll('.js-ajax-template');
    for (var i = 0; i < newTemplates.length; i++) {
      var newTemplate = newTemplates[i];
      if (!document.getElementById(newTemplate.id)) {
        document.body.appendChild(newTemplate);
      }
    }
    animatePageIn(pageName);
  }

  function runPageHandler(funcName, pageName) {
    var page = IOWA.Elements.Template.pages[pageName];
    if (page && page[funcName]) {
      // If page we're going to has a load handler, run it.
      page[funcName]();
    }
  }

  /**
   * Initialized ajax-based routing on the page.
   */
  function init() {
    window.addEventListener('popstate', function(e) {
      var currentPage = IOWA.Elements.Template.selectedPage;
      var nextPage = parsePageNameFromAbsolutePath(window.location.pathname);

      // Note: popstate is fired when history.pushState() is called so we need
      // to determine if the event was organic (browser back/forward button).
      // If it was, currentPage has not been updated yet and is the previous page.
      if (!navigationFromLinkClick) {
        runPageHandler('unload', currentPage);
      }

      navigationFromLinkClick = false; // Reset
      IOWA.Elements.Template.scrollLock(false); // Ensure main scroll container can scroll again.

      // Ignore the navigation if it was a hash update, but on the same page.
      if (e.state && e.state.fromHashChange && nextPage === currentPage) {
        return;
      }
      document.title = (IOWA.Elements.Template.pages[nextPage].title ||
          'Google I/O 2015');
      IOWA.Analytics.trackPageView(e.state && e.state.path);
      renderCurrentPage();
    });

    // On iOS, we don't have event bubbling to the document level.
    // http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
    var eventName = IOWA.Util.isIOS() || IOWA.Util.isTouchScreen() ?
        'touchstart' : 'click';
    document.addEventListener(eventName, navigate);
  }

  return {
    init: init,
    getPageName: parsePageNameFromAbsolutePath,
    animatePageIn: animatePageIn
  };

})();
<commit_msg>cleanup<commit_after>/**
 * Copyright 2015 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

 /**
  * @fileOverview The ajax-based routing for IOWA subpages.
  */

IOWA.Router = (function() {

  "use strict";

  var MASTHEAD_BG_CLASS_REGEX = /(\s|^)bg-[a-z-]+(\s|$)/;

  /**
   * Tells what kind of transition is currently happening on the page,
   * e.g. 'hero-card-transition' or 'masthead-ripple-transition'.
   * @type {=string?}
   */
  var currentPageTransition = null;

  /**
   * True if the URL update was due to a user click. Used to differentiate a
   * popstate event from a link click and a history pop.
   * @type {boolean}
   */
  var navigationFromLinkClick = false;

  /**
   * Navigates to a new page via a hero card takeover transition.
   * @param {Event} e Event that triggered navigation.
   * @param {Element} el Element clicked.
   * @param {string} rippleColor Color of the ripple on the card.
   * @private
   */
  function playHeroTransition(e, el, rippleColor) {
    // TODO: This may need some perf tweaking for FF.
    var card = null;
    var currentEl = el;
    while (!card) {
      currentEl = currentEl.parentNode;
      if (currentEl.classList.contains('card__container')) {
        card = currentEl;
      }
    }
    IOWA.PageAnimation.play(
      IOWA.PageAnimation.pageCardTakeoverOut(
          card, e.pageX, e.pageY, 300, rippleColor),
      function() {
        IOWA.History.pushState({'path': el.pathname}, '', el.href);
      }
    );
  }

  /**
   * Navigates to a new page via a masthead nav item ripple transition.
   * @param {Event} e Event that triggered navigation.
   * @param {Element} el Element clicked.
   * @param {string} mastheadColor Color of the masthead.
   * @param {string} rippleColor Color of the ripple.
   * @param {boolean} isFadeRipple If true, ripple will just glimpse and fade.
   * @private
   */
  function playMastheadRippleTransition(
      e, el, mastheadColor, rippleColor, isFadeRipple) {
    var x = e.touches ? e.touches[0].pageX : e.pageX;
    var y = e.touches ? e.touches[0].pageY : e.pageY;
    var duration = isFadeRipple ? 300 : 600;
    var rippleAnim = IOWA.PageAnimation.ripple(
          IOWA.Elements.Ripple, x, y, duration,
          rippleColor, isFadeRipple);
    var animGroup = [
      rippleAnim,
      IOWA.PageAnimation.contentSlideOut(),
    ];
    if (!isFadeRipple) {
      var mastheadAnim = new Animation(IOWA.Elements.Masthead, [
          {backgroundColor: mastheadColor},
          {backgroundColor: rippleColor}
        ], {
          duration: 300,
          delay: 0,
          fill: 'forwards'  // Makes ripple keep its state after animation.
      });
      animGroup.push(mastheadAnim);
    }
    var animation = new AnimationGroup(animGroup);
    IOWA.PageAnimation.play(animation, function() {
      IOWA.History.pushState({'path': el.pathname}, '', el.href);
    });
  }

  /**
   * Navigates to a new page via ajax and page transitions.
   * @param {Event} e Event that triggered navigation.
   * @param {Element} el Element clicked.
   * @param {String} currentPage Current page id.
   * @param {String} nextPage Id of the page the user navigates to.
   * @private
   */
  function handleAjaxLink(e, el, currentPage, nextPage) {
    e.preventDefault();
    e.stopPropagation();

    var template = IOWA.Elements.Template;
    var bgClass = template.pages[nextPage] &&
                  template.pages[nextPage].mastheadBgClass;
    var prevBgClass = template.pages[currentPage].mastheadBgClass;

    template.navBgClass = bgClass;

    var isFadeRipple = prevBgClass === bgClass;
    var mastheadColor = template.rippleColors[prevBgClass];
    var rippleColor = isFadeRipple ? '#fff' : template.rippleColors[bgClass];

    if (currentPage !== nextPage) {
      IOWA.Elements.Template.fire('page-transition-start');
      if (el.hasAttribute('data-anim-ripple')) {
        currentPageTransition = 'masthead-ripple-transition';
        playMastheadRippleTransition(
            e, el, mastheadColor, rippleColor, isFadeRipple);
      } else if (el.hasAttribute('data-anim-drawer'))  {
        var handler = function(e) {
          e.target.removeEventListener('core-transitionend', handler);
          currentPageTransition = '';
          IOWA.History.pushState({'path': el.pathname}, '', el.href);
        };
        el.parentNode.addEventListener('core-transitionend', handler);
      } else if (el.hasAttribute('data-anim-card'))  {
        currentPageTransition = 'hero-card-transition';
        playHeroTransition(e, el, rippleColor);
      } else {
        currentPageTransition = '';
        IOWA.History.pushState({'path': el.pathname}, '', el.href);
      }
    }
    // TODO: Update meta.
  }

  /**
   * Navigates to a new page. Uses ajax for data-ajax-link links.
   * @param {Event} e Event that triggered navigation.
   * @private
   */
  function navigate(e) {
    // Allow user to open page in a new tab.
    if (e.metaKey || e.ctrlKey) {
      return;
    }

    navigationFromLinkClick = true;

    // Inject page if <a> has the data-ajax-link attribute.
    for (var i = 0; i < e.path.length; ++i) {
      var el = e.path[i];
      if (el.localName === 'a') {
        var currentPage = parsePageNameFromAbsolutePath(location.pathname);
        var nextPage = parsePageNameFromAbsolutePath(el.pathname);

        // First, record click event if link requests it.
        if (el.hasAttribute('data-track-link')) {
          IOWA.Analytics.trackEvent(
              'link', 'click', el.getAttribute('data-track-link'));
        }

        // Ignore links that go offsite.
        if (el.target) {
          return;
        }

        // Prevent navigations to the same page.
        // Note, this prevents in-page anchors. Use IOWA.Util.smoothScroll.
        if (currentPage === nextPage) {
          e.preventDefault();
          return;
        }

        // Do ajax page navigation if link requests it.
        if (el.hasAttribute('data-ajax-link')) {
          runPageHandler('unload', currentPage);
          handleAjaxLink(e, el, currentPage, nextPage);
        } else {
          currentPageTransition = 'no-transition';
        }

        return; // found first navigation element, quit here.
      }
    }
  }

  /**
   * Renders a new page by fetching partials through ajax.
   * @param {string} pageName The name of the new page.
   * @private
   */
  function renderPage(pageName) {
    var importURL = pageName + '?partial';
    // TODO(ericbidelman): update call when github.com/Polymer/polymer/pull/1128 lands.
    Polymer.import([importURL], function() {
      // Don't proceed if import didn't load correctly.
      var htmlImport = document.querySelector(
          'link[rel="import"][href="' + importURL + '"]');
      if (htmlImport && !htmlImport.import) {
        return;
      }

      // FF doesn't execute the <script> inside the main content <template>
      // (inside page partial import). Instead, the first time the partial is
      // loaded, find any script tags in and make them runnable by appending them back to the template.
      if (IOWA.Util.isFF() || IOWA.Util.isIE()) {
        var contentTemplate = document.querySelector(
           '#template-' + pageName + '-content');
        if (!contentTemplate) {
          var containerTemplate = htmlImport.import.querySelector(
              '[data-ajax-target-template="template-content-container"]');
          var scripts = containerTemplate.content.querySelectorAll('script');
          Array.prototype.forEach.call(scripts, function(node, i) {
            replaceScriptTagWithRunnableScript(node);
          });
        }
      }

      // Update content of the page.
      injectPageContent(pageName, htmlImport.import);
    });
  }

  /**
   * Replaces in-page <script> tag in xhr'd body content with runnable script.
   *
   * @param {Node} node Container element to replace script content.
   * @private
   */
  function replaceScriptTagWithRunnableScript(node) {
    var script = document.createElement('script');
    script.text = node.text || node.textContent || node.innerHTML;

    // IE doesn't execute the script when it's appended to the middle
    // of the DOM. Append it to body instead, then remove.
    if (IOWA.Util.isIE()) {
      document.body.appendChild(script);
      document.body.removeChild(script);
    } else {
      node.parentNode.replaceChild(script, node); // FF
    }
  }

  /**
   * Replaces templated content.
   * @private
   */
  function replaceTemplateContent(currentPageTemplates) {
    for (var j = 0; j < currentPageTemplates.length; j++) {
      var template = currentPageTemplates[j];
      var templateToReplace = document.getElementById(
          template.getAttribute('data-ajax-target-template'));
      if (templateToReplace) {
        templateToReplace.setAttribute('ref', template.id);
      }
    }
  }

  /**
   * Updates the page elements during the page transition.
   * @param {string} pageName New page identifier.
   * @param {NodeList} currentPageTemplates Content templates to be rendered.
   * @private
   */
  function updatePageElements(pageName, currentPageTemplates) {
    replaceTemplateContent(currentPageTemplates);

    var template = IOWA.Elements.Template;

    // Update menu/drawer selected item.
    template.selectedPage = pageName;
    IOWA.Elements.DrawerMenu.selected = pageName;

    var currentPageMeta = template.pages[pageName];
    document.body.id = 'page-' + pageName;
    document.title = currentPageMeta.title || 'Google I/O 2015';

    var previousPageMeta = template.pages[template.selectedPage];
    var previousMastheadColor = template.rippleColors[previousPageMeta.mastheadBgClass];
    var currentMastheadColor = template.rippleColors[currentPageMeta.mastheadBgClass];

    // Prepare the page for a smooth masthead transition.
    var mastheadBgClass = currentPageMeta.mastheadBgClass;
    template.navBgClass = mastheadBgClass;
    // This cannot be updated via data binding, because the masthead
    // is visible before the binding happens.
    IOWA.Elements.Masthead.className = IOWA.Elements.Masthead.className.replace(
        MASTHEAD_BG_CLASS_REGEX, ' ' + mastheadBgClass + ' ');

    // Transition masthead color.
    IOWA.PageAnimation.play(new Animation(IOWA.Elements.Masthead, [
      {backgroundColor: previousMastheadColor},
      {backgroundColor: currentMastheadColor}
    ], {
      duration: currentPageTransition ? 0 : 300,
      fill: 'forwards'
    }));

    // Hide the masthead ripple before proceeding with page transition.
    IOWA.PageAnimation.play(
        IOWA.PageAnimation.elementFadeOut(IOWA.Elements.Ripple, {duration: 0}));

    // Scroll to top of new page.
    IOWA.Elements.ScrollContainer.scrollTop = 0;

    // Wait 1 rAF for DOM to settle.
    IOWA.Elements.Template.async(function() {
      var animationFunc;
      if (currentPageTransition === 'hero-card-transition') {
        animationFunc = IOWA.PageAnimation.pageCardTakeoverIn;
      } else {
        animationFunc = IOWA.PageAnimation.pageSlideIn;
      }

      IOWA.PageAnimation.play(animationFunc(), function() {
        // Fire event when the page transitions are final.
        IOWA.Elements.Template.fire('page-transition-done');
      });

      currentPageTransition = '';
    });
  }

  /**
   * Runs animated page transition.
   * @param {string} pageName New page identifier.
   * @private
   */
  function animatePageIn(pageName) {
    // Prequery for content templates.
    var currentPageTemplates = document.querySelectorAll(
        '.js-ajax-' + pageName);
    if (!currentPageTransition) {
      var animation = IOWA.PageAnimation.contentSlideOut();
      IOWA.PageAnimation.play(animation, updatePageElements.bind(
          null, pageName, currentPageTemplates));
    } else if (currentPageTransition !== 'no-transition') {
      updatePageElements(pageName, currentPageTemplates);
    }
  }

  /**
   * Parses the page name out of the last entry in absolutePath, split on '/'.
   * Defaults to 'home' if absolutePath ends in '/' or is ''.
   * @private
   */
  function parsePageNameFromAbsolutePath(absolutePath) {
    return absolutePath.split('/').pop() || 'home';
  }

  /**
   * Renders a new page for the current location.
   * @private
   */
  function renderCurrentPage() {
    renderPage(parsePageNameFromAbsolutePath(window.location.pathname));
  }

  /**
   * Injects new page content into existing layout.
   * @param {string} pageName New page identifier.
   * @param {DocumentFragment} importContent HTML containing templates to be
   *    injected.
   * @private
   */
  function injectPageContent(pageName, importContent) {

    runPageHandler('load', pageName);

    // Add freshly fetched templates to DOM, if not yet present.
    var newTemplates = importContent.querySelectorAll('.js-ajax-template');
    for (var i = 0; i < newTemplates.length; i++) {
      var newTemplate = newTemplates[i];
      if (!document.getElementById(newTemplate.id)) {
        document.body.appendChild(newTemplate);
      }
    }
    animatePageIn(pageName);
  }

  function runPageHandler(funcName, pageName) {
    var page = IOWA.Elements.Template.pages[pageName];
    if (page && page[funcName]) {
      // If page we're going to has a load handler, run it.
      page[funcName]();
    }
  }

  /**
   * Initialized ajax-based routing on the page.
   */
  function init() {
    window.addEventListener('popstate', function(e) {
      var currentPage = IOWA.Elements.Template.selectedPage;
      var nextPage = parsePageNameFromAbsolutePath(window.location.pathname);

      // Note: popstate is fired when history.pushState() is called so we need
      // to determine if the event was organic (browser back/forward button).
      // If it was, currentPage has not been updated yet and is the previous page.
      if (!navigationFromLinkClick) {
        runPageHandler('unload', currentPage);
      }

      navigationFromLinkClick = false; // Reset
      IOWA.Elements.Template.scrollLock(false); // Ensure main scroll container can scroll again.

      // Ignore the navigation if it was a hash update, but on the same page.
      if (e.state && e.state.fromHashChange && nextPage === currentPage) {
        return;
      }
      document.title = (IOWA.Elements.Template.pages[nextPage].title ||
          'Google I/O 2015');
      IOWA.Analytics.trackPageView(e.state && e.state.path);
      renderCurrentPage();
    });

    // On iOS, we don't have event bubbling to the document level.
    // http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
    var eventName = IOWA.Util.isIOS() || IOWA.Util.isTouchScreen() ?
        'touchstart' : 'click';
    document.addEventListener(eventName, navigate);
  }

  return {
    init: init,
    getPageName: parsePageNameFromAbsolutePath,
    animatePageIn: animatePageIn
  };

})();
<|endoftext|>


<commit_before>/** @format */
const korpApp = angular.module("korpApp")
korpApp.factory("kwicDownload", function() {
    const emptyRow = function(length) {
        return _.fill(new Array(length), "")
    }

    const createFile = function(dataType, fileType, content) {
        const date = moment().format("YYYYDDMM_HHmmss")
        const filename = `korp_${dataType}_${date}.${fileType}`
        const blobURL = window.URL.createObjectURL(
            new Blob([content], { type: `text/${fileType}` })
        )
        return [filename, blobURL]
    }

    const padRows = (data, length) =>
        _.map(data, function(row) {
            const newRow = emptyRow(length)
            newRow[0] = row
            return newRow
        })

    const createSearchInfo = function(requestInfo, totalHits) {
        const rows = []
        const fields = ["cqp", "context", "within", "sorting", "start", "end", "hits"]
        for (let field of fields) {
            var row
            if (field === "cqp") {
                row = `## CQP query: ${requestInfo.cqp}`
            }
            if (field === "context") {
                row = `## context: ${requestInfo.default_context}`
            }
            if (field === "within") {
                row = `## within: ${requestInfo.default_within}`
            }
            if (field === "sorting") {
                const sorting = requestInfo.sort || "none"
                row = `## sorting: ${sorting}`
            }
            if (field === "start") {
                row = `## start: ${requestInfo.start}`
            }
            if (field === "end") {
                const cqpQuery = ""
                row = `## end: ${requestInfo.end}`
            }
            if (field === "hits") {
                row = `## Total hits: ${totalHits}`
            }
            rows.push(row)
        }
        return rows
    }

    const transformDataToAnnotations = function(data, searchInfo) {
        const headers = _.filter(
            _.keys(data[1].tokens[0]),
            val =>
                val.indexOf("_") !== 0 &&
                val !== "structs" &&
                val !== "$$hashKey" &&
                val !== "position"
        )
        const columnCount = headers.length + 1
        let corpus
        const res = padRows(searchInfo, columnCount)
        res.push(["match"].concat(headers))
        for (let row of data) {
            if (row.tokens) {
                const textAttributes = []
                for (let attrName in row.structs) {
                    const attrValue = row.structs[attrName]
                    textAttributes.push(attrName + ': "' + attrValue + '"')
                }
                const hitInfo = emptyRow(columnCount)
                hitInfo[0] = `# ${corpus}; text attributes: ${textAttributes.join(", ")}`
                res.push(hitInfo)

                for (let token of row.tokens || []) {
                    var match
                    if (token.position >= row.match.start && token.position < row.match.end) {
                        match = "***"
                    } else {
                        match = ""
                    }
                    const newRow = [match]
                    for (let field of headers) {
                        newRow.push(token[field])
                    }
                    res.push(newRow)
                }
            } else if (row.newCorpus) {
                corpus = row.newCorpus
            }
        }

        return res
    }

    const transformDataToKWIC = function(data, searchInfo) {
        let row
        let corpus
        const structHeaders = []
        let res = []
        for (row of data) {
            if (row.tokens) {
                var attrName, token
                const leftContext = []
                for (token of row.tokens.slice(0, row.match.start)) {
                    leftContext.push(token.word)
                }
                const match = []
                for (token of row.tokens.slice(row.match.start, row.match.end)) {
                    match.push(token.word)
                }
                const rightContext = []
                for (token of row.tokens.slice(row.match.end, row.tokens.length)) {
                    rightContext.push(token.word)
                }

                const structs = []
                for (attrName in row.structs) {
                    if (!structHeaders.includes(attrName)) {
                        structHeaders.push(attrName)
                    }
                }
                for (attrName of structHeaders) {
                    if (attrName in row.structs) {
                        structs.push(row.structs[attrName])
                    } else {
                        structs.push("")
                    }
                }

                const newRow = [
                    corpus,
                    row.match.position,
                    leftContext.join(" "),
                    match.join(" "),
                    rightContext.join(" ")
                ].concat(structs)
                res.push(newRow)
            } else if (row.newCorpus) {
                corpus = row.newCorpus
            }
        }

        const headers = [
            "corpus",
            "match_position",
            "left context",
            "match",
            "right_context"
        ].concat(structHeaders)
        res = [headers].concat(res)

        res.push(emptyRow(headers.length))
        for (let row of padRows(searchInfo, headers.length)) {
            res.push(row)
        }

        return res
    }

    const transformData = function(dataType, data, requestInfo, totalHits) {
        const searchInfo = createSearchInfo(requestInfo, totalHits)
        if (dataType === "annotations") {
            return transformDataToAnnotations(data, searchInfo)
        }
        if (dataType === "kwic") {
            return transformDataToKWIC(data, searchInfo)
        }
    }

    const makeContent = function(fileType, transformedData) {
        let dataDelimiter
        if (fileType === "csv") {
            dataDelimiter = ","
        }
        if (fileType === "tsv") {
            dataDelimiter = "   "
        }

        const csv = new CSV(transformedData, {
            delimiter: dataDelimiter
        })

        return csv.encode()
    }

    // dataType: either "kwic" or "annotations"
    // fileType: either "csv" or "tsv"
    // data: json from the backend
    return {
        makeDownload(dataType, fileType, data, requestInfo, totalHits) {
            const tmp = transformData(dataType, data, requestInfo, totalHits)
            const tmp2 = makeContent(fileType, tmp)
            return createFile(dataType, fileType, tmp2)
        }
    }
})
<commit_msg>fix #40<commit_after>/** @format */
const korpApp = angular.module("korpApp")
korpApp.factory("kwicDownload", function() {
    const emptyRow = function(length) {
        return _.fill(new Array(length), "")
    }

    const createFile = function(dataType, fileType, content) {
        const date = moment().format("YYYYDDMM_HHmmss")
        const filename = `korp_${dataType}_${date}.${fileType}`
        const blobURL = window.URL.createObjectURL(
            new Blob([content], { type: `text/${fileType}` })
        )
        return [filename, blobURL]
    }

    const padRows = (data, length) =>
        _.map(data, function(row) {
            const newRow = emptyRow(length)
            newRow[0] = row
            return newRow
        })

    const createSearchInfo = function(requestInfo, totalHits) {
        const rows = []
        const fields = ["cqp", "context", "within", "sorting", "start", "end", "hits"]
        for (let field of fields) {
            var row
            if (field === "cqp") {
                row = `## CQP query: ${requestInfo.cqp}`
            }
            if (field === "context") {
                row = `## context: ${requestInfo.default_context}`
            }
            if (field === "within") {
                row = `## within: ${requestInfo.default_within}`
            }
            if (field === "sorting") {
                const sorting = requestInfo.sort || "none"
                row = `## sorting: ${sorting}`
            }
            if (field === "start") {
                row = `## start: ${requestInfo.start}`
            }
            if (field === "end") {
                const cqpQuery = ""
                row = `## end: ${requestInfo.end}`
            }
            if (field === "hits") {
                row = `## Total hits: ${totalHits}`
            }
            rows.push(row)
        }
        return rows
    }

    const transformDataToAnnotations = function(data, searchInfo) {
        const headers = _.filter(
            _.keys(data[1].tokens[0]),
            val =>
                val.indexOf("_") !== 0 &&
                val !== "structs" &&
                val !== "$$hashKey" &&
                val !== "position"
        )
        const columnCount = headers.length + 1
        let corpus
        const res = padRows(searchInfo, columnCount)
        res.push(["match"].concat(headers))
        for (let row of data) {
            if (row.tokens) {
                const textAttributes = []
                for (let attrName in row.structs) {
                    const attrValue = row.structs[attrName]
                    textAttributes.push(attrName + ': "' + attrValue + '"')
                }
                const hitInfo = emptyRow(columnCount)
                hitInfo[0] = `# ${corpus}; text attributes: ${textAttributes.join(", ")}`
                res.push(hitInfo)

                for (let token of row.tokens || []) {
                    var match
                    if (token.position >= row.match.start && token.position < row.match.end) {
                        match = "***"
                    } else {
                        match = ""
                    }
                    const newRow = [match]
                    for (let field of headers) {
                        newRow.push(token[field])
                    }
                    res.push(newRow)
                }
            } else if (row.newCorpus) {
                corpus = row.newCorpus
            }
        }

        return res
    }

    const transformDataToKWIC = function(data, searchInfo) {
        let row
        let corpus
        const structHeaders = []
        let res = []
        for (row of data) {
            if (row.tokens) {
                if (row.isLinked) {
                    // parallell mode does not have matches or structs for the linked sentences
                    // current wordaround is to add all tokens to the left context
                    res.push(["", "", row.tokens.map(token => token.word).join(" "), "", ""])
                    continue
                }

                var attrName, token
                const leftContext = []
                const match = []
                const rightContext = []

                for (token of row.tokens.slice(0, row.match.start)) {
                    leftContext.push(token.word)
                }
                for (token of row.tokens.slice(row.match.start, row.match.end)) {
                    match.push(token.word)
                }
                for (token of row.tokens.slice(row.match.end, row.tokens.length)) {
                    rightContext.push(token.word)
                }

                const structs = []
                for (attrName in row.structs) {
                    if (!structHeaders.includes(attrName)) {
                        structHeaders.push(attrName)
                    }
                }
                for (attrName of structHeaders) {
                    if (attrName in row.structs) {
                        structs.push(row.structs[attrName])
                    } else {
                        structs.push("")
                    }
                }
                const newRow = [
                    corpus,
                    row.match.position,
                    leftContext.join(" "),
                    match.join(" "),
                    rightContext.join(" ")
                ].concat(structs)
                res.push(newRow)
            } else if (row.newCorpus) {
                corpus = row.newCorpus
            }
        }

        const headers = [
            "corpus",
            "match_position",
            "left context",
            "match",
            "right_context"
        ].concat(structHeaders)
        res = [headers].concat(res)

        res.push(emptyRow(headers.length))
        for (let row of padRows(searchInfo, headers.length)) {
            res.push(row)
        }

        return res
    }

    const transformData = function(dataType, data, requestInfo, totalHits) {
        const searchInfo = createSearchInfo(requestInfo, totalHits)
        if (dataType === "annotations") {
            return transformDataToAnnotations(data, searchInfo)
        }
        if (dataType === "kwic") {
            return transformDataToKWIC(data, searchInfo)
        }
    }

    const makeContent = function(fileType, transformedData) {
        let dataDelimiter
        if (fileType === "csv") {
            dataDelimiter = ","
        }
        if (fileType === "tsv") {
            dataDelimiter = "   "
        }

        const csv = new CSV(transformedData, {
            delimiter: dataDelimiter
        })

        return csv.encode()
    }

    // dataType: either "kwic" or "annotations"
    // fileType: either "csv" or "tsv"
    // data: json from the backend
    return {
        makeDownload(dataType, fileType, data, requestInfo, totalHits) {
            const tmp = transformData(dataType, data, requestInfo, totalHits)
            const tmp2 = makeContent(fileType, tmp)
            return createFile(dataType, fileType, tmp2)
        }
    }
})
<|endoftext|>