<commit_before>;

// daguerre jQuery namespace
var daguerre = {
    "jQuery": jQuery || django.jQuery.noConflict(true) || grp.jQuery.noConflict(true),
    "area_containers": [],
};

daguerre.jQuery(function($){
    function Area(conf, container){
        var area = this,
            cell_names = ['name', 'priority', 'x1', 'y1', 'x2', 'y2', 'edit', 'delete'];
        if (conf.x1 === undefined || conf.x2 === undefined || conf.y1 === undefined || conf.y2 === undefined) {
            throw "Missing coordinates for area.";
        };
        if (!conf.storage_path) {
            throw "Invalid storage path for area.";
        };
        area.save_url = container.ele.data('area-url');
        area.img = container.img;
        area.row = $('

');

        area.cells = {};
        $.each(cell_names, function(){
            area.cells[this] = $('	');
            area.row.append(area.cells[this]);
        });
        area.inputs = {
            name: $('<input />'),
            priority: $('<input type="number" min=1 />'),
        };
        area.cells.name.html(area.inputs.name);
        area.cells.priority.html(area.inputs.priority);

        area.editing = false;
        area.saving = false;

        area.delete_link = $('Delete');
        area.delete_link_click = function(e){
            e.preventDefault();
            area.remove();
        };

        area.edit_link = $('Edit');
        area.edit_link_click = function(e){
            e.preventDefault();
            area.startEdit();
        };

        area.done_link = $('Done');
        area.done_link_click = function(e){
            e.preventDefault();
            area.stopEdit();
        }

        area.display = function() {
            area.inputs.name.val(area.name);
            area.inputs.priority.val(area.priority);
            area.cells.x1.html(area.x1);
            area.cells.y1.html(area.y1);
            area.cells.x2.html(area.x2);
            area.cells.y2.html(area.y2);

            // Set up delete link.
            area.cells['delete'].html(area.delete_link);
            area.delete_link.click(area.delete_link_click);

            if (area.saving) {
                area.cells.edit.html('Saving...');
            } else if (area.editing) {
                area.cells.edit.html(area.done_link);
                area.done_link.click(area.done_link_click);
            } else {
                area.cells.edit.html(area.edit_link);
                area.edit_link.click(area.edit_link_click);
            };
        };

        area.setSelection = function(selection) {
            area.x1 = selection.x1;
            area.y1 = selection.y1;
            area.x2 = selection.x2;
            area.y2 = selection.y2;
        };

        area.init = function(conf) {
            area.setSelection(conf);
            area.priority = conf.priority || 3;
            area.name = conf.name || '';
            area.id = conf.id || null;
            area.storage_path = conf.storage_path;
            area.display()
        };
        area.init(conf);

        area.serialize = function(){
            return {
                x1: area.x1,
                y1: area.y1,
                x2: area.x2,
                y2: area.y2,
                priority: area.priority,
                name: area.name,
                id: area.id || '',
                storage_path: area.storage_path,
            }
        };

        area.startEdit = function() {
            if (area.editing) return;
            area.editing = true;
            $.each(container.areas, function(index, a){
                if (a.editing && !(a === area)){
                    a.stopEdit();
                }
            });
            area.img.imgAreaSelect({remove: true});
            area.img.imgAreaSelect({
                handles: true,
                parent: container.ele,
                x1: area.x1,
                y1: area.y1,
                x2: area.x2,
                y2: area.y2,
                persistent: true,
                onSelectChange: function(img, selection){
                    area.setSelection(selection);
                    area.display();
                },
                onSelectEnd: function(img, selection){
                    area.setSelection(selection);
                    area.save();
                },
                imageWidth: container.ele.data('width'),
                imageHeight: container.ele.data('height'),
            });
            area.display();
        };

        area.stopEdit = function() {
            if (area.id === null) {
                area.save();
            };
            area.editing = false;
            area.img.imgAreaSelect({remove: true});
            area.display();
            container.imgInit();
        }

        area.save = function() {
            var url = area.save_url;
            if (area.id != null) {
                url += '/' + area.id;
            };

            // Fetch the name/priority values.
            area.name = area.row.find('input').first().val();
            area.priority = area.row.find('input').last().val();

            area.saving = true;
            area.display();
            $.post(url, area.serialize(), function(data){
                area.saving = false;
                area.init(data);
            });
        };

        area.remove = function() {
            var url = area.save_url;
            if (area.id != null) {
                url += '/' + area.id;
                $.ajax(url, {type: 'DELETE'});
            };
            area.row.remove();
            container.imgInit();

            var index = container.areas.indexOf(area);
            if (index != -1) {
                container.areas.splice(index, 1);
            };
        };

        area.row.find('input').focus(function(){
            var $this = $(this);
            area.startEdit();
            $this.data('value', $this.val());
        });
        area.row.find('input').blur(function(){
            var $this = $(this);
            if ($this.val() != $this.data('value')) {
                area.save();
            };
        });
        area.row.find('input').keydown(function(e){
            if (e.which == 13) {
                e.preventDefault();
            };
        })
    };

    function AreaContainer(ele){
        var container = this,
            areas = container.areas = [],
            img = container.img = $(''),
            add_link = container.add_link = $(''),
            table = container.table = $(
                '

' +
                'Name	' +
                'Priority	' +
                'x1	' +
                'y1	' +
                'x2	' +
                'y2	' +
                '	' +
                '

');

        container.ele = ele;
        container.ele.append(img);
        container.ele.append(table);
        table.find('tr').children().last().append(add_link);

        container.addArea = function(conf, activate) {
            var area = new Area(conf, container),
                activate = activate === undefined ? true : false;
            areas.push(area);
            table.append(area.row);
            if (activate) {area.startEdit();};
            return area;
        };

        container.imgInit = function() {
            container.img.imgAreaSelect({remove: true});
            container.img.imgAreaSelect({
                parent: ele,
                onSelectEnd: function(img, selection){
                    var area = container.addArea({
                        x1: selection.x1,
                        y1: selection.y1,
                        x2: selection.x2,
                        y2: selection.y2,
                        storage_path: ele.data('storage-path'),
                    });
                    area.save();
                    area.startEdit();
                },
                imageWidth: ele.data('width'),
                imageHeight: ele.data('height'),
            });

        };

        container.init = function() {
            add_link.click(function(e){
                e.preventDefault();
                var area = container.addArea({
                    storage_path: ele.data('storage-path'),
                    x1: 0,
                    y1: 0,
                    x2: ele.data('width'),
                    y2: ele.data('height'),
                });
            });

            $.getJSON(ele.data('url'), {w: 540, h: 300, a: 'fit'}, function(data){
                img.attr('src', data['url']);
                img.attr('width', data['width']);
                img.attr('height', data['height']);
                container.imgInit()
            });
            $.getJSON(ele.data('area-url'), function(data){
                $.each(data, function(index, conf){
                    container.addArea(conf, false);
                });
            });
        };
        container.init()
    };
    $('.daguerre-areas').each(function(){
        daguerre.area_containers.push(new AreaContainer($(this)));
    });

    // Set csrf token on post requests.
    function getCookie(name) {
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = $.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }
    var csrftoken = getCookie('csrftoken');

    function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }
    function sameOrigin(url) {
        // test that a given url is a same-origin URL
        // url could be relative or scheme relative or absolute
        var host = document.location.host; // host + port
        var protocol = document.location.protocol;
        var sr_origin = '//' + host;
        var origin = protocol + sr_origin;
        // Allow absolute or scheme relative URLs to same origin
        return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
            (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
            // or any other URL that isn't scheme relative or absolute i.e relative.
            !(/^(\/\/|http:|https:).*/.test(url));
    }
    $.ajaxSetup({
        beforeSend: function(xhr, settings) {
            if (!csrfSafeMethod(settings.type) && sameOrigin(settings.url)) {
                // Send the token to same-origin, relative URLs only.
                // Send the token only if the method warrants CSRF protection
                // Using the CSRFToken value acquired earlier
                xhr.setRequestHeader("X-CSRFToken", csrftoken);
            }
        }
    });
});<commit_msg>Fixed the areawidget javascript to use the new ajax syntax.<commit_after>;

// daguerre jQuery namespace
var daguerre = {
    "jQuery": jQuery || django.jQuery.noConflict(true) || grp.jQuery.noConflict(true),
    "area_containers": [],
};

daguerre.jQuery(function($){
    function Area(conf, container){
        var area = this,
            cell_names = ['name', 'priority', 'x1', 'y1', 'x2', 'y2', 'edit', 'delete'];
        if (conf.x1 === undefined || conf.x2 === undefined || conf.y1 === undefined || conf.y2 === undefined) {
            throw "Missing coordinates for area.";
        };
        if (!conf.storage_path) {
            throw "Invalid storage path for area.";
        };
        area.save_url = container.ele.data('area-url');
        area.img = container.img;
        area.row = $('

');

        area.cells = {};
        $.each(cell_names, function(){
            area.cells[this] = $('	');
            area.row.append(area.cells[this]);
        });
        area.inputs = {
            name: $('<input />'),
            priority: $('<input type="number" min=1 />'),
        };
        area.cells.name.html(area.inputs.name);
        area.cells.priority.html(area.inputs.priority);

        area.editing = false;
        area.saving = false;

        area.delete_link = $('Delete');
        area.delete_link_click = function(e){
            e.preventDefault();
            area.remove();
        };

        area.edit_link = $('Edit');
        area.edit_link_click = function(e){
            e.preventDefault();
            area.startEdit();
        };

        area.done_link = $('Done');
        area.done_link_click = function(e){
            e.preventDefault();
            area.stopEdit();
        }

        area.display = function() {
            area.inputs.name.val(area.name);
            area.inputs.priority.val(area.priority);
            area.cells.x1.html(area.x1);
            area.cells.y1.html(area.y1);
            area.cells.x2.html(area.x2);
            area.cells.y2.html(area.y2);

            // Set up delete link.
            area.cells['delete'].html(area.delete_link);
            area.delete_link.click(area.delete_link_click);

            if (area.saving) {
                area.cells.edit.html('Saving...');
            } else if (area.editing) {
                area.cells.edit.html(area.done_link);
                area.done_link.click(area.done_link_click);
            } else {
                area.cells.edit.html(area.edit_link);
                area.edit_link.click(area.edit_link_click);
            };
        };

        area.setSelection = function(selection) {
            area.x1 = selection.x1;
            area.y1 = selection.y1;
            area.x2 = selection.x2;
            area.y2 = selection.y2;
        };

        area.init = function(conf) {
            area.setSelection(conf);
            area.priority = conf.priority || 3;
            area.name = conf.name || '';
            area.id = conf.id || null;
            area.storage_path = conf.storage_path;
            area.display()
        };
        area.init(conf);

        area.serialize = function(){
            return {
                x1: area.x1,
                y1: area.y1,
                x2: area.x2,
                y2: area.y2,
                priority: area.priority,
                name: area.name,
                id: area.id || '',
                storage_path: area.storage_path,
            }
        };

        area.startEdit = function() {
            if (area.editing) return;
            area.editing = true;
            $.each(container.areas, function(index, a){
                if (a.editing && !(a === area)){
                    a.stopEdit();
                }
            });
            area.img.imgAreaSelect({remove: true});
            area.img.imgAreaSelect({
                handles: true,
                parent: container.ele,
                x1: area.x1,
                y1: area.y1,
                x2: area.x2,
                y2: area.y2,
                persistent: true,
                onSelectChange: function(img, selection){
                    area.setSelection(selection);
                    area.display();
                },
                onSelectEnd: function(img, selection){
                    area.setSelection(selection);
                    area.save();
                },
                imageWidth: container.ele.data('width'),
                imageHeight: container.ele.data('height'),
            });
            area.display();
        };

        area.stopEdit = function() {
            if (area.id === null) {
                area.save();
            };
            area.editing = false;
            area.img.imgAreaSelect({remove: true});
            area.display();
            container.imgInit();
        }

        area.save = function() {
            var url = area.save_url;
            if (area.id != null) {
                url += '/' + area.id;
            };

            // Fetch the name/priority values.
            area.name = area.row.find('input').first().val();
            area.priority = area.row.find('input').last().val();

            area.saving = true;
            area.display();
            $.post(url, area.serialize(), function(data){
                area.saving = false;
                area.init(data);
            });
        };

        area.remove = function() {
            var url = area.save_url;
            if (area.id != null) {
                url += '/' + area.id;
                $.ajax(url, {type: 'DELETE'});
            };
            area.row.remove();
            container.imgInit();

            var index = container.areas.indexOf(area);
            if (index != -1) {
                container.areas.splice(index, 1);
            };
        };

        area.row.find('input').focus(function(){
            var $this = $(this);
            area.startEdit();
            $this.data('value', $this.val());
        });
        area.row.find('input').blur(function(){
            var $this = $(this);
            if ($this.val() != $this.data('value')) {
                area.save();
            };
        });
        area.row.find('input').keydown(function(e){
            if (e.which == 13) {
                e.preventDefault();
            };
        })
    };

    function AreaContainer(ele){
        var container = this,
            areas = container.areas = [],
            img = container.img = $(''),
            add_link = container.add_link = $(''),
            table = container.table = $(
                '

' +
                'Name	' +
                'Priority	' +
                'x1	' +
                'y1	' +
                'x2	' +
                'y2	' +
                '	' +
                '

');

        container.ele = ele;
        container.ele.append(img);
        container.ele.append(table);
        table.find('tr').children().last().append(add_link);

        container.addArea = function(conf, activate) {
            var area = new Area(conf, container),
                activate = activate === undefined ? true : false;
            areas.push(area);
            table.append(area.row);
            if (activate) {area.startEdit();};
            return area;
        };

        container.imgInit = function() {
            container.img.imgAreaSelect({remove: true});
            container.img.imgAreaSelect({
                parent: ele,
                onSelectEnd: function(img, selection){
                    var area = container.addArea({
                        x1: selection.x1,
                        y1: selection.y1,
                        x2: selection.x2,
                        y2: selection.y2,
                        storage_path: ele.data('storage-path'),
                    });
                    area.save();
                    area.startEdit();
                },
                imageWidth: ele.data('width'),
                imageHeight: ele.data('height'),
            });

        };

        container.init = function() {
            add_link.click(function(e){
                e.preventDefault();
                var area = container.addArea({
                    storage_path: ele.data('storage-path'),
                    x1: 0,
                    y1: 0,
                    x2: ele.data('width'),
                    y2: ele.data('height'),
                });
            });

            $.getJSON(ele.data('url'), {r: "fit|540|300"}, function(data){
                img.attr('src', data['url']);
                img.attr('width', data['width']);
                img.attr('height', data['height']);
                container.imgInit()
            });
            $.getJSON(ele.data('area-url'), function(data){
                $.each(data, function(index, conf){
                    container.addArea(conf, false);
                });
            });
        };
        container.init()
    };
    $('.daguerre-areas').each(function(){
        daguerre.area_containers.push(new AreaContainer($(this)));
    });

    // Set csrf token on post requests.
    function getCookie(name) {
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = $.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }
    var csrftoken = getCookie('csrftoken');

    function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }
    function sameOrigin(url) {
        // test that a given url is a same-origin URL
        // url could be relative or scheme relative or absolute
        var host = document.location.host; // host + port
        var protocol = document.location.protocol;
        var sr_origin = '//' + host;
        var origin = protocol + sr_origin;
        // Allow absolute or scheme relative URLs to same origin
        return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
            (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
            // or any other URL that isn't scheme relative or absolute i.e relative.
            !(/^(\/\/|http:|https:).*/.test(url));
    }
    $.ajaxSetup({
        beforeSend: function(xhr, settings) {
            if (!csrfSafeMethod(settings.type) && sameOrigin(settings.url)) {
                // Send the token to same-origin, relative URLs only.
                // Send the token only if the method warrants CSRF protection
                // Using the CSRFToken value acquired earlier
                xhr.setRequestHeader("X-CSRFToken", csrftoken);
            }
        }
    });
});<|endoftext|>


<commit_before>
/*
 * Customization options available -
 *
 * tala pattern = || , , , , | , , | , , ||
 * aksharas per line = 8
 * aksharas = aksharas per line
 * line spacing = 22
 * para spacing = 22
 * notation font size = 13
 * notation small font size = 11
 * notation font = serif
 * text font = serif
 * stretch = 1.0
 * stretch space = 1.0
 * margin top = 22
 * margin left = 10
 * line extension top = 0
 * line extension bottom = 5
 *
 */
function RenderSVG(window, paragraphs, style) {

    var keyTalaPattern = '$tala pattern';
    var keyAksharasPerLine = '$aksharas per line';
    var keyAksharas = '$aksharas';
    var keyLineSpacing = '$line spacing';
    var keyParaSpacing = '$para spacing';
    var keyNotationFontSize = '$notation font size';
    var keyNotationSmallFontSize = '$notation small font size';
    var keyNotationFont = '$notation font';
    var keyTextFont = '$text font';
    var keyStretch = '$stretch';
    var keyStretchSpace = '$stretch space';
    var keyMarginTop = '$margin top';
    var keyMarginLeft = '$margin left';
    var keyLineExtensionTop = '$line extension top';
    var keyLineExtensionBottom = '$line extension bottom';

    var talaCache = {};
    var cursor = {x: 0, y: 0, xmax: 0, ymax: 0};
    var pendingLines = {};

    loadStyleDefaults(style);

    cursor.y = style[keyMarginTop];
    cursor.x = style[keyMarginLeft];

    processTalaPatterns(paragraphs);

    var div = window.document.createElement('div');
    var svg = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    div.insertBefore(svg, null);

    paragraphs.forEach(typesetPara);

    svg.setAttribute('width', cursor.xmax);
    svg.setAttribute('height', cursor.ymax);

    function extendLine(ix, x, fromY, toY) {
        var l = pendingLines[ix];
        if (!l) {
            l = pendingLines[ix] = {x: x, fromY: fromY, toY: toY};
        }

        l.toY = toY;
    }

    function flushLines() {
        var ix, l;
        for (ix in pendingLines) {
            if (pendingLines.hasOwnProperty(ix)) {
                l = pendingLines[ix];
                svgelem(svg, 'line', {
                    x1: l.x,
                    y1: l.fromY,
                    x2: l.x,
                    y2: l.toY,
                    'stroke-width': 2,
                    stroke: 'black'
                });
            }
        }
        pendingLines = {};
    }

    function typesetPara(para, i) {
        if (para.lines) {
            para.lines.forEach(function (line) { typesetLine(para, line); });
            flushLines();
            if (i + 1 < paragraphs.length) {
                nextLine(keyParaSpacing);
            }
        }
    }

    function typesetLine(para, line) {
        switch (line.type) {
            case 'text':
                flushLines();
                typesetText(para, line);
                break;
            case 'svarasthana':
                typesetSvarasthana(para, line);
                break;
            case 'lyrics':
                typesetLyrics(para, line);
                break;
            default:
                throw new Error('Unknown line type ' + line.type);
        }
    }

    function typesetText(para, line) {
        svgelem(svg, 'text', {x: cursor.x, y: cursor.y, style: ('font-family: ' + style[keyTextFont] + ';') + 'font-size: 12pt;'}, line.tokens.join(' '));
        nextLine();
        cursor.y += style[keyLineExtensionTop] + style[keyLineExtensionBottom];
    }

    function typesetSvarasthana(para, line) {
        typesetTimedText(para, line);
    }

    function getSubSvaras(word) {
        return word.match(/([SrRgGmMPdDnNS][\+\-]*)|[,_]/g) || [];
    }

    function getSyllables(word) {
        var parts = [];
        var lastIx, i, N, c;
        for (i = 0, lastIx = 0, N = word.length; i < N; ++i) {
            c = word.charAt(i);
            if (c === ',' || c === '_') {
                if (i > lastIx) {
                    parts.push(word.substring(lastIx, i));
                }
                lastIx = i;
            }
        }
        if (i > lastIx) {
            parts.push(word.substring(lastIx, i));
        }
        return parts;
    }

    function typesetTimedText(para, line, additionalTextStyle) {
        var tala = para.properties[keyTalaPattern];
        var akshIx = 0, instrIx = 0, tokIx = 0, instr;

        // Skip aksharas until start
        while (akshIx < para.tala_interval.from) {
            if (tala.instructions[instrIx].tick) {
                akshIx++;
            }
            instrIx++;
        }

        // Skip all spaces
        while (instrIx < tala.instructions.length && tala.instructions[instrIx].space) {
            ++instrIx;
        }

        var givenAksharas = +(para.properties[keyAksharas] || para.properties[keyAksharasPerLine]);
        var startAkshIx = akshIx;
        var startInstrIx = instrIx;
        var subdivs = line.tokens.length / givenAksharas;
        var stretch = (+para.properties[keyStretch]) * style[keyStretch];
        var stretchSpace = stretch * ((+para.properties[keyStretchSpace]) || style[keyStretchSpace]);
        var textStyleBase = ('font-family:' + (line.type === 'lyrics' ? 'serif' : style[keyNotationFont]) + ';') + (additionalTextStyle || '');
        var smallFontSize = (+(para.properties[keyNotationSmallFontSize] || style[keyNotationSmallFontSize]));
        var fontSize = function (n) {
//            return 'font-size:' + Math.max(8,Math.ceil(style[keyNotationFontSize]/Math.log(Math.max(1,n)))) + 'pt;';
            return 'font-size: ' + (n > 2 ? smallFontSize : style[keyNotationFontSize]) + 'pt;';
        };
        var textStyle = textStyleBase + fontSize(subdivs);
        var subDivIx = 0;
        var subsvaras, props, dx;
        var isSvarasthana = line.type === 'svarasthana';

        REQUIRE(subdivs % 1.0 < 0.00001, "Invalid subdivision");
        subdivs = Math.floor(subdivs);
        props = {x: cursor.x, y: cursor.y, style: textStyle};

        var renderSubsvara = function (s) {
            svgelem(svg, 'text', props, show(s));
            props.x += dx / subsvaras.length;
        };

        while (akshIx < para.tala_interval.to) {
            instr = tala.instructions[instrIx];
            if (instr.tick) {
                dx = instr.tick * stretch / subdivs;
                props.x = cursor.x;
                props.y = cursor.y;
                props.style = textStyle;
                if (isSvarasthana) {
                    subsvaras = getSubSvaras(line.tokens[tokIx]);
                    props.style = textStyleBase + fontSize((subsvaras.length > 2 ? subsvaras.length : 1) * subdivs);
                    subsvaras.forEach(renderSubsvara);
                } else {
                    subsvaras = getSyllables(line.tokens[tokIx]);
                    props.style = textStyleBase + fontSize(subdivs);
                    subsvaras.forEach(renderSubsvara);
                }
                cursor.x += dx;
                ++subDivIx;
                ++tokIx;
                if (subDivIx >= subdivs) {
                    subDivIx = 0;
                    ++akshIx;
                    ++instrIx;
                } else {
                    continue;
                }
            }

            // Consume other non-tick instructions up to the next tick.
            while (instrIx < tala.instructions.length && (instr = tala.instructions[instrIx], !instr.tick)) {
                if (instr.space) {
                    if (instr.scale) {
                        cursor.x += instr.space * stretchSpace;
                    } else {
                        cursor.x += instr.space;
                    }
                } else if (instr.line) {
                    if (instr.draw) {
                        extendLine(instrIx,
                                cursor.x,
                                cursor.y - style[keyLineSpacing] - style[keyLineExtensionTop],
                                cursor.y + style[keyLineExtensionBottom]);
                    }
                    cursor.x += instr.line;
                }
                instr = tala.instructions[++instrIx];
            }
        }

        nextLine();
    }

    function typesetLyrics(para, line) {
        typesetTimedText(para, line, 'font-style: italic;');
    }

    function nextLine(spacingKey) {
        cursor.y += style[spacingKey || keyLineSpacing];
        cursor.ymax = Math.max(cursor.y, cursor.ymax);
        cursor.xmax = Math.max(cursor.x + 40, cursor.xmax);
        cursor.x = style[keyMarginLeft];
    }

    // Augment each "paragraph" with information about
    // where in the tala cycle it begins and where it
    // ends.
    //
    // Adds "tala_interval" property, which is an object
    // of the form {from: m, to:n}, where the aksharas
    // included are k >= m & k < n.
    function processTalaPatterns(paragraphs) {
        var fromAkshara = 0, toAkshara = 0, aksharas = 0, aksharasInTala = 0, aksharasPerLine = 0;

        var i, N, para, tala, aksh;

        for (i = 0, N = paragraphs.length; i < N; ++i) {

            para = paragraphs[i];
            tala = para.properties[keyTalaPattern];

            aksharasInTala = countAksharasInTala(tala);
            aksharasPerLine = +(para.properties[keyAksharasPerLine]);
            aksharas = +(para.properties[keyAksharas] || para.properties[keyAksharasPerLine]);

            toAkshara += aksharas;

            // Repeat the tala pattern enough times to cover the given notation line.
            aksh = aksharasInTala;
            while (toAkshara > aksh) {
                para.properties[keyTalaPattern] += tala;
                aksh += aksharasInTala;
            }

            // On repetition, you may find patterns such as '||||' in the tala.
            // This we normalize to '||' - i.e. the maximum number of times a vertical
            // bar may be repeated is 2.
            tala = para.properties[keyTalaPattern] = compileTala(para.properties[keyTalaPattern].replace(/\|\|+/g, '||'));

            var paraAksharas = +(para.properties[keyAksharas] || para.properties[keyAksharasPerLine]);

            // Adjust notation lines whose akshara count is smaller than
            // the given aksharas per line. We do this by stretching the
            // line by an integer factor.
            para.lines.forEach(function (line) {
                if (line.type !== 'text') {
                    if (line.tokens.length < paraAksharas) {
                        ASSERT(aksh % line.tokens.length === 0);
                        var i, j, M, N, newTokens = [];
                        M = aksh / line.tokens.length;
                        for (i = 0, N = line.tokens.length; i < N; ++i) {
                            newTokens.push(line.tokens[i]);
                            for (j = 1; j < M; ++j) {
                                newTokens.push('_');
                            }
                        }
                        line.tokens = newTokens;
                    }
                }
            });

            para.tala_interval = {
                from: fromAkshara,
                to: toAkshara
            };

            // Modulo tala cycle.
            fromAkshara = toAkshara;
            while (fromAkshara >= aksharasInTala) {
                fromAkshara -= aksharasInTala;
                toAkshara -= aksharasInTala;
            }
        }

        return paragraphs;
    }

    function compileTala(talaPattern) {
        var talaKey = '$' + talaPattern;

        if (talaKey in talaCache) {
            return talaCache[talaKey];
        }

        var i, N, c, instrs = [], nearLine;
        for (i = 0, N = talaPattern.length; i < N; ++i) {
            c = talaPattern.charAt(i);
            nearLine = (i + 1 < N && talaPattern.charAt(i+1) === '|') || (i > 0 && talaPattern.charAt(i-1) === '|');
            switch (c) {
                case '|' :
                    if (nearLine) {
                        instrs.push({line: 5, draw: true});
                    } else {
                        instrs.push({line: 10, draw: true});
                    }
                    break;
                case ' ':
                    if (nearLine) {
                        instrs.push({space: 15, scale: false});
                    } else {
                        instrs.push({space: 30, scale: true});
                    }
                    break;
                case ',':
                    instrs.push({tick: 40});
                    break;
                case '_':
                    instrs.push({line: 10, draw: false});
                    break;
                default:
                    throw new Error('Unknown pattern character [' + c + '] in tala - "' + talaPattern + '"');
            }
        }

        return (talaCache[talaKey] = {
            pattern: talaPattern,
            aksharas: countAksharasInTala(talaPattern),
            instructions: instrs
        });
    }

    function countAksharasInTala(talaPattern) {
        var commas = talaPattern.match(/,/g);
        var num = (commas && commas.length) || 0;
        REQUIRE(num > 0, "At least one akshara is required in a tala");
        return num;
    }

    function show(text) {
        var hisa = "Ṡ";
        var losa = "Ṣ";

        if (text === '_') {
            return "";
        }

        if (text.length === 2 && text[1] === '+') {
            return text[0] + hisa[1];
        }

        if (text.length === 2 && text[1] === '-') {
            return text[0] + losa[1];
        }

        return text;
    }

    function svgelem(elem, n, attrs, content) {
        var tag = window.document.createElementNS('http://www.w3.org/2000/svg', n);
        if (attrs) {
            Object.keys(attrs).forEach(function (k) {
                if (attrs[k]) {
                    tag.setAttribute(k, attrs[k]);
                }
            });
        }
        if (content) {
            tag.textContent = content;
        }
        elem.appendChild(tag);
        return tag;
    }

    function loadStyleDefaults(style) {
        style[keyLineSpacing] = (+style[keyLineSpacing]) || 22;
        style[keyParaSpacing] = (+style[keyParaSpacing]) || style[keyLineSpacing];
        style[keyNotationFontSize] = (+style[keyNotationFontSize]) || 13;
        style[keyNotationSmallFontSize] = (+style[keyNotationSmallFontSize]) || (style[keyNotationFontSize] - 2);
        style[keyNotationFont] = (style[keyNotationFont] || 'sans-serif');
        style[keyTextFont] = (style[keyTextFont] || 'serif');
        style[keyStretch] = (+style[keyStretch]) || 1.0;
        style[keyStretchSpace] = (+style[keyStretchSpace]) || 1.0;
        style[keyMarginTop] = (+style[keyMarginTop]) || style[keyLineSpacing];
        style[keyMarginLeft] = (+style[keyMarginLeft]) || 10;
        style[keyLineExtensionTop] = (+style[keyLineExtensionTop]) || 0;
        style[keyLineExtensionBottom] = (+style[keyLineExtensionBottom]) || 5;
    }

    return svg;
}
<commit_msg>BUGFIX: Error in splitting a word using spacing characters is now fixed.<commit_after>
/*
 * Customization options available -
 *
 * tala pattern = || , , , , | , , | , , ||
 * aksharas per line = 8
 * aksharas = aksharas per line
 * line spacing = 22
 * para spacing = 22
 * notation font size = 13
 * notation small font size = 11
 * notation font = serif
 * text font = serif
 * stretch = 1.0
 * stretch space = 1.0
 * margin top = 22
 * margin left = 10
 * line extension top = 0
 * line extension bottom = 5
 *
 */
function RenderSVG(window, paragraphs, style) {

    var keyTalaPattern = '$tala pattern';
    var keyAksharasPerLine = '$aksharas per line';
    var keyAksharas = '$aksharas';
    var keyLineSpacing = '$line spacing';
    var keyParaSpacing = '$para spacing';
    var keyNotationFontSize = '$notation font size';
    var keyNotationSmallFontSize = '$notation small font size';
    var keyNotationFont = '$notation font';
    var keyTextFont = '$text font';
    var keyStretch = '$stretch';
    var keyStretchSpace = '$stretch space';
    var keyMarginTop = '$margin top';
    var keyMarginLeft = '$margin left';
    var keyLineExtensionTop = '$line extension top';
    var keyLineExtensionBottom = '$line extension bottom';

    var talaCache = {};
    var cursor = {x: 0, y: 0, xmax: 0, ymax: 0};
    var pendingLines = {};

    loadStyleDefaults(style);

    cursor.y = style[keyMarginTop];
    cursor.x = style[keyMarginLeft];

    processTalaPatterns(paragraphs);

    var div = window.document.createElement('div');
    var svg = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    div.insertBefore(svg, null);

    paragraphs.forEach(typesetPara);

    svg.setAttribute('width', cursor.xmax);
    svg.setAttribute('height', cursor.ymax);

    function extendLine(ix, x, fromY, toY) {
        var l = pendingLines[ix];
        if (!l) {
            l = pendingLines[ix] = {x: x, fromY: fromY, toY: toY};
        }

        l.toY = toY;
    }

    function flushLines() {
        var ix, l;
        for (ix in pendingLines) {
            if (pendingLines.hasOwnProperty(ix)) {
                l = pendingLines[ix];
                svgelem(svg, 'line', {
                    x1: l.x,
                    y1: l.fromY,
                    x2: l.x,
                    y2: l.toY,
                    'stroke-width': 2,
                    stroke: 'black'
                });
            }
        }
        pendingLines = {};
    }

    function typesetPara(para, i) {
        if (para.lines) {
            para.lines.forEach(function (line) { typesetLine(para, line); });
            flushLines();
            if (i + 1 < paragraphs.length) {
                nextLine(keyParaSpacing);
            }
        }
    }

    function typesetLine(para, line) {
        switch (line.type) {
            case 'text':
                flushLines();
                typesetText(para, line);
                break;
            case 'svarasthana':
                typesetSvarasthana(para, line);
                break;
            case 'lyrics':
                typesetLyrics(para, line);
                break;
            default:
                throw new Error('Unknown line type ' + line.type);
        }
    }

    function typesetText(para, line) {
        svgelem(svg, 'text', {x: cursor.x, y: cursor.y, style: ('font-family: ' + style[keyTextFont] + ';') + 'font-size: 12pt;'}, line.tokens.join(' '));
        nextLine();
        cursor.y += style[keyLineExtensionTop] + style[keyLineExtensionBottom];
    }

    function typesetSvarasthana(para, line) {
        typesetTimedText(para, line);
    }

    function getSubSvaras(word) {
        return word.match(/([SrRgGmMPdDnNS][\+\-]*)|[,_]/g) || [];
    }

    function getSyllables(word) {
        var parts = [];
        var lastIx, i, N, c;
        for (i = 0, lastIx = 0, N = word.length; i < N; ++i) {
            c = word.charAt(i);
            if (c === ',' || c === '_') {
                if (i > lastIx) {
                    parts.push(word.substring(lastIx, i));
                }
                lastIx = i + 1;
                parts.push(c);
            }
        }
        if (i > lastIx) {
            parts.push(word.substring(lastIx, i));
        }
        return parts;
    }

    function typesetTimedText(para, line, additionalTextStyle) {
        var tala = para.properties[keyTalaPattern];
        var akshIx = 0, instrIx = 0, tokIx = 0, instr;

        // Skip aksharas until start
        while (akshIx < para.tala_interval.from) {
            if (tala.instructions[instrIx].tick) {
                akshIx++;
            }
            instrIx++;
        }

        // Skip all spaces
        while (instrIx < tala.instructions.length && tala.instructions[instrIx].space) {
            ++instrIx;
        }

        var givenAksharas = +(para.properties[keyAksharas] || para.properties[keyAksharasPerLine]);
        var startAkshIx = akshIx;
        var startInstrIx = instrIx;
        var subdivs = line.tokens.length / givenAksharas;
        var stretch = (+para.properties[keyStretch]) * style[keyStretch];
        var stretchSpace = stretch * ((+para.properties[keyStretchSpace]) || style[keyStretchSpace]);
        var textStyleBase = ('font-family:' + (line.type === 'lyrics' ? 'serif' : style[keyNotationFont]) + ';') + (additionalTextStyle || '');
        var smallFontSize = (+(para.properties[keyNotationSmallFontSize] || style[keyNotationSmallFontSize]));
        var fontSize = function (n) {
//            return 'font-size:' + Math.max(8,Math.ceil(style[keyNotationFontSize]/Math.log(Math.max(1,n)))) + 'pt;';
            return 'font-size: ' + (n > 2 ? smallFontSize : style[keyNotationFontSize]) + 'pt;';
        };
        var textStyle = textStyleBase + fontSize(subdivs);
        var subDivIx = 0;
        var subsvaras, props, dx;
        var isSvarasthana = line.type === 'svarasthana';

        REQUIRE(subdivs % 1.0 < 0.00001, "Invalid subdivision");
        subdivs = Math.floor(subdivs);
        props = {x: cursor.x, y: cursor.y, style: textStyle};

        var renderSubsvara = function (s) {
            svgelem(svg, 'text', props, show(s));
            props.x += dx / subsvaras.length;
        };

        while (akshIx < para.tala_interval.to) {
            instr = tala.instructions[instrIx];
            if (instr.tick) {
                dx = instr.tick * stretch / subdivs;
                props.x = cursor.x;
                props.y = cursor.y;
                props.style = textStyle;
                if (isSvarasthana) {
                    subsvaras = getSubSvaras(line.tokens[tokIx]);
                    props.style = textStyleBase + fontSize((subsvaras.length > 2 ? subsvaras.length : 1) * subdivs);
                    subsvaras.forEach(renderSubsvara);
                } else {
                    subsvaras = getSyllables(line.tokens[tokIx]);
                    props.style = textStyleBase + fontSize(subdivs);
                    subsvaras.forEach(renderSubsvara);
                }
                cursor.x += dx;
                ++subDivIx;
                ++tokIx;
                if (subDivIx >= subdivs) {
                    subDivIx = 0;
                    ++akshIx;
                    ++instrIx;
                } else {
                    continue;
                }
            }

            // Consume other non-tick instructions up to the next tick.
            while (instrIx < tala.instructions.length && (instr = tala.instructions[instrIx], !instr.tick)) {
                if (instr.space) {
                    if (instr.scale) {
                        cursor.x += instr.space * stretchSpace;
                    } else {
                        cursor.x += instr.space;
                    }
                } else if (instr.line) {
                    if (instr.draw) {
                        extendLine(instrIx,
                                cursor.x,
                                cursor.y - style[keyLineSpacing] - style[keyLineExtensionTop],
                                cursor.y + style[keyLineExtensionBottom]);
                    }
                    cursor.x += instr.line;
                }
                instr = tala.instructions[++instrIx];
            }
        }

        nextLine();
    }

    function typesetLyrics(para, line) {
        typesetTimedText(para, line, 'font-style: italic;');
    }

    function nextLine(spacingKey) {
        cursor.y += style[spacingKey || keyLineSpacing];
        cursor.ymax = Math.max(cursor.y, cursor.ymax);
        cursor.xmax = Math.max(cursor.x + 40, cursor.xmax);
        cursor.x = style[keyMarginLeft];
    }

    // Augment each "paragraph" with information about
    // where in the tala cycle it begins and where it
    // ends.
    //
    // Adds "tala_interval" property, which is an object
    // of the form {from: m, to:n}, where the aksharas
    // included are k >= m & k < n.
    function processTalaPatterns(paragraphs) {
        var fromAkshara = 0, toAkshara = 0, aksharas = 0, aksharasInTala = 0, aksharasPerLine = 0;

        var i, N, para, tala, aksh;

        for (i = 0, N = paragraphs.length; i < N; ++i) {

            para = paragraphs[i];
            tala = para.properties[keyTalaPattern];

            aksharasInTala = countAksharasInTala(tala);
            aksharasPerLine = +(para.properties[keyAksharasPerLine]);
            aksharas = +(para.properties[keyAksharas] || para.properties[keyAksharasPerLine]);

            toAkshara += aksharas;

            // Repeat the tala pattern enough times to cover the given notation line.
            aksh = aksharasInTala;
            while (toAkshara > aksh) {
                para.properties[keyTalaPattern] += tala;
                aksh += aksharasInTala;
            }

            // On repetition, you may find patterns such as '||||' in the tala.
            // This we normalize to '||' - i.e. the maximum number of times a vertical
            // bar may be repeated is 2.
            tala = para.properties[keyTalaPattern] = compileTala(para.properties[keyTalaPattern].replace(/\|\|+/g, '||'));

            var paraAksharas = +(para.properties[keyAksharas] || para.properties[keyAksharasPerLine]);

            // Adjust notation lines whose akshara count is smaller than
            // the given aksharas per line. We do this by stretching the
            // line by an integer factor.
            para.lines.forEach(function (line) {
                if (line.type !== 'text') {
                    if (line.tokens.length < paraAksharas) {
                        ASSERT(aksh % line.tokens.length === 0);
                        var i, j, M, N, newTokens = [];
                        M = aksh / line.tokens.length;
                        for (i = 0, N = line.tokens.length; i < N; ++i) {
                            newTokens.push(line.tokens[i]);
                            for (j = 1; j < M; ++j) {
                                newTokens.push('_');
                            }
                        }
                        line.tokens = newTokens;
                    }
                }
            });

            para.tala_interval = {
                from: fromAkshara,
                to: toAkshara
            };

            // Modulo tala cycle.
            fromAkshara = toAkshara;
            while (fromAkshara >= aksharasInTala) {
                fromAkshara -= aksharasInTala;
                toAkshara -= aksharasInTala;
            }
        }

        return paragraphs;
    }

    function compileTala(talaPattern) {
        var talaKey = '$' + talaPattern;

        if (talaKey in talaCache) {
            return talaCache[talaKey];
        }

        var i, N, c, instrs = [], nearLine;
        for (i = 0, N = talaPattern.length; i < N; ++i) {
            c = talaPattern.charAt(i);
            nearLine = (i + 1 < N && talaPattern.charAt(i+1) === '|') || (i > 0 && talaPattern.charAt(i-1) === '|');
            switch (c) {
                case '|' :
                    if (nearLine) {
                        instrs.push({line: 5, draw: true});
                    } else {
                        instrs.push({line: 10, draw: true});
                    }
                    break;
                case ' ':
                    if (nearLine) {
                        instrs.push({space: 15, scale: false});
                    } else {
                        instrs.push({space: 30, scale: true});
                    }
                    break;
                case ',':
                    instrs.push({tick: 40});
                    break;
                case '_':
                    instrs.push({line: 10, draw: false});
                    break;
                default:
                    throw new Error('Unknown pattern character [' + c + '] in tala - "' + talaPattern + '"');
            }
        }

        return (talaCache[talaKey] = {
            pattern: talaPattern,
            aksharas: countAksharasInTala(talaPattern),
            instructions: instrs
        });
    }

    function countAksharasInTala(talaPattern) {
        var commas = talaPattern.match(/,/g);
        var num = (commas && commas.length) || 0;
        REQUIRE(num > 0, "At least one akshara is required in a tala");
        return num;
    }

    function show(text) {
        var hisa = "Ṡ";
        var losa = "Ṣ";

        if (text === '_') {
            return "";
        }

        if (text.length === 2 && text[1] === '+') {
            return text[0] + hisa[1];
        }

        if (text.length === 2 && text[1] === '-') {
            return text[0] + losa[1];
        }

        return text;
    }

    function svgelem(elem, n, attrs, content) {
        var tag = window.document.createElementNS('http://www.w3.org/2000/svg', n);
        if (attrs) {
            Object.keys(attrs).forEach(function (k) {
                if (attrs[k]) {
                    tag.setAttribute(k, attrs[k]);
                }
            });
        }
        if (content) {
            tag.textContent = content;
        }
        elem.appendChild(tag);
        return tag;
    }

    function loadStyleDefaults(style) {
        style[keyLineSpacing] = (+style[keyLineSpacing]) || 22;
        style[keyParaSpacing] = (+style[keyParaSpacing]) || style[keyLineSpacing];
        style[keyNotationFontSize] = (+style[keyNotationFontSize]) || 13;
        style[keyNotationSmallFontSize] = (+style[keyNotationSmallFontSize]) || (style[keyNotationFontSize] - 2);
        style[keyNotationFont] = (style[keyNotationFont] || 'sans-serif');
        style[keyTextFont] = (style[keyTextFont] || 'serif');
        style[keyStretch] = (+style[keyStretch]) || 1.0;
        style[keyStretchSpace] = (+style[keyStretchSpace]) || 1.0;
        style[keyMarginTop] = (+style[keyMarginTop]) || style[keyLineSpacing];
        style[keyMarginLeft] = (+style[keyMarginLeft]) || 10;
        style[keyLineExtensionTop] = (+style[keyLineExtensionTop]) || 0;
        style[keyLineExtensionBottom] = (+style[keyLineExtensionBottom]) || 5;
    }

    return svg;
}
<|endoftext|>


<commit_before>/*!
 * SVG.js Pan Zoom Plugin
 * ======================
 *
 * A JavaScript library for pan and zoom SVG things.
 * Created with <3 and JavaScript by the jillix developers.
 *
 * svg.pan-zoom.js 2.2.0
 * Licensed under the MIT license.
 * */
;(function() {

    var container = null
      , markers = null
      , mousewheel = "onwheel" in document.createElement("div")
      ? "wheel"
      : document.onmousewheel !== undefined
      ? "mousewheel"
      : "DOMMouseScroll"
      ;

    /**
     * panZoom
     * The pan-zoom contructor.
     *
     * @name panZoom
     * @function
     * @param {Object|undefined} opt_options An optional object containing the following fields:
     *
     *  - `zoom` (Array): An array of two float values: the minimum and maximum zoom values (default: `undefined`).
     *  - `zoomSpeed` (Number): The zoom speed (default: `-1`). By changing the sign the zoom is reversed.
     *
     * @return {PanZoom} The PanZoom object containing the following fields:
     *
     *  - `elm` (SVG): The selected element.
     *  - `pan` (Object): An object containing pan values.
     *  - `transform` (Object): An object containing the transform data (`scaleX`, `scaleY`, `x` and `y`).
     *
     */
    function panZoom(opt_options) {

        // Selected element
        var self = this;

        /**
         * setPosition
         * Sets the graph position programatically.
         *
         * @name setPosition
         * @function
         * @param {Number} x The relative position to the svg document (on x axis).
         * @param {Number} y The relative position to the svg document (on y axis).
         * @param {Number} z The zoom value which will be handled as `scale` internally.
         * @return {PanZoom} The `PanZoom` instance.
         */
        function setPosition(x, y, z) {
            pz.pan.iPos = pz.pan.fPos;
            pz.transform = self.transform();
            pz.transform.x = x;
            pz.transform.y = y;
            if (typeof z === "number") {
                pz.zoom(z);
            } else {
                updateMatrix();
            }
            return pz;
        }

        /**
         * zoom
         * Zooms in/out the graph programatically.
         *
         * @name zoom
         * @function
         * @param {Number} z The zoom value which will be handled as `scale` internally.
         * @return {PanZoom} The `PanZoom` instance.
         */
        function zoom(z, oX, oY, ev) {

            // Handle zoom restrictions
            if (z > opt_options.zoom[1] || z < opt_options.zoom[0]) {
                return;
            }

            if (arguments.length === 1) {
                pz.transform = pz.transform || self.transform();
                pz.transform.scaleY = pz.transform.scaleX = z;
                updateMatrix();
                return pz;
            }

            var tr = pz.transform

                // Get the current x, y
              , currentX = tr.x
              , currentY = tr.y
              , scaleD = z / tr.scaleX

                // Compute the final x, y
              , x = scaleD * (currentX - oX) + oX
              , y = scaleD * (currentY - oY) + oY
              ;

            setPosition(x, y, z);

            if (ev) {
                self.node.dispatchEvent(new CustomEvent("zoom", { detail: { e: ev, tr: tr } }));
            }
        }

        // Pan zoom object
        var pz = {
            pan: {}
          , elm: self
          , setPosition: setPosition
          , zoom: zoom
        };

        // Set options
        opt_options = Object(opt_options);
        opt_options.zoom = opt_options.zoom || [];
        opt_options.zoomSpeed = typeof opt_options.zoomSpeed === "number" ? opt_options.zoomSpeed : -1;

        // Get the svg document
        var svg = self;
        while ((svg = self.parent()).node.tagName.toUpperCase() !== "SVG") {}

        // Create the rectangle
        var rect = new SVG(document.createDocumentFragment()).rect().attr({
            width: svg.width()
          , height: svg.height()
          , fill: "none"
        }).style("pointer-events", "all");

        // Insert the rectangle
        self.parent().node.insertBefore(rect.node, self.node);

        /*!
         * updateMatrix
         * An internal function called to update the svg matrix.
         *
         * @name updateMatrix
         * @function
         * @return {undefined}
         */
        function updateMatrix() {
            self.attr("transform", "matrix(" + [
                pz.transform.scaleX
              , 0, 0
              , pz.transform.scaleY
              , pz.transform.x
              , pz.transform.y
            ].join(",")+ ")");
        }

        /*!
         * pan
         * The internal function called for panning.
         *
         * @name pan
         * @function
         * @param {Event} e The internal listener event.
         * @return {undefined}
         */
        function pan(e) {
            if (!pz.pan.mousedown) {
                return;
            }
            var tr = pz.transform = self.transform();
            var diffX = pz.pan.fPos.x - pz.pan.iPos.x;
            var diffY = pz.pan.fPos.y - pz.pan.iPos.y;
            pz.setPosition(tr.x + diffX, tr.y + diffY);
            self.node.dispatchEvent(new CustomEvent("pan", { detail: { e: e, tr: tr } }));
        }

        /*!
         * mousePos
         * Returns the mouse point coordinates.
         *
         * @name mousePos
         * @function
         * @param {Event} e The mouse event.
         * @param {Boolean} rel If `true`, the relative coordinates will be returned instead.
         * @return {Object} An object containing the relative coordinates.
         */
        function mousePos(e, rel) {
            var bbox = self.parent().node.getBoundingClientRect()
              , abs = {
                    x: e.clientX || e.touches[0].pageX
                  , y: e.clientY || e.touches[0].pageY
                }
              ;
            if (!rel) { return abs; }
            return {
                x: abs.x - bbox.left
              , y: abs.y - bbox.top
            };
        }

        /*!
         * zoom
         * The internal function called for zooming.
         *
         * @name zoom
         * @function
         * @param {Event} e The internal listener event.
         * @return {undefined}
         */
        function doZoom (e) {

            // Get the relative mouse point
            var rP = mousePos(e, true)
              , oX = rP.x
              , oY = rP.y
              ;

            e.deltaY = e.deltaY || e.wheelDeltaY;

            // Compute the new scale
            var d = opt_options.zoomSpeed * e.deltaY / 1000
              , tr = pz.transform = self.transform()
              , scale = tr.scaleX + (tr.scaleX * d)
              ;

            zoom(scale, oX, oY, e);

            // Prevent the default browser behavior
            e.preventDefault();
        }

        // The event listeners
        var EventListeners = {
            mouse_down: function (e) {
                pz.pan.mousedown = true;
                pz.pan.iPos = mousePos(e);
            }
          , mouse_up: function (e) {
                pz.pan.mousedown = false;
                pz.pan.fPos = mousePos(e);
                pan();
            }
          , mouse_move: function (e) {
                if (!pz.pan.mousedown) { return; }
                pz.pan.fPos = mousePos(e);
                pan();
            }
          , mouse_leave: function (e) {
                pz.pan.mousedown = false;
            }
        };

        // Add event listeners
        rect
          .on(mousewheel, doZoom)
          .on("mousedown", EventListeners.mouse_down)
          .on("touchstart", EventListeners.mouse_down)
          .on("mousemove", EventListeners.mouse_move)
          .on("touchmove", EventListeners.mouse_move)
          .on("mouseup", EventListeners.mouse_up)
          .on("touchup", EventListeners.mouse_up)
          .on("mouseleave", EventListeners.mouse_leave)
          ;

        self.on(mousewheel, doZoom);
        return pz;
    }

    // Extend the SVG.Element with the new function
    SVG.extend(SVG.Element, {
        panZoom: panZoom
    });
}).call(this);
<commit_msg>JSDoc comments<commit_after>/*!
 * SVG.js Pan Zoom Plugin
 * ======================
 *
 * A JavaScript library for pan and zoom SVG things.
 * Created with <3 and JavaScript by the jillix developers.
 *
 * svg.pan-zoom.js 2.2.0
 * Licensed under the MIT license.
 * */
;(function() {

    var container = null
      , markers = null
      , mousewheel = "onwheel" in document.createElement("div")
      ? "wheel"
      : document.onmousewheel !== undefined
      ? "mousewheel"
      : "DOMMouseScroll"
      ;

    /**
     * panZoom
     * The pan-zoom contructor.
     *
     * @name panZoom
     * @function
     * @param {Object|undefined} opt_options An optional object containing the following fields:
     *
     *  - `zoom` (Array): An array of two float values: the minimum and maximum zoom values (default: `undefined`).
     *  - `zoomSpeed` (Number): The zoom speed (default: `-1`). By changing the sign the zoom is reversed.
     *
     * @return {PanZoom} The PanZoom object containing the following fields:
     *
     *  - `elm` (SVG): The selected element.
     *  - `pan` (Object): An object containing pan values.
     *  - `transform` (Object): An object containing the transform data (`scaleX`, `scaleY`, `x` and `y`).
     *
     */
    function panZoom(opt_options) {

        // Selected element
        var self = this;

        /**
         * setPosition
         * Sets the graph position programatically.
         *
         * @name setPosition
         * @function
         * @param {Number} x The relative position to the svg document (on x axis).
         * @param {Number} y The relative position to the svg document (on y axis).
         * @param {Number} z The zoom value which will be handled as `scale` internally.
         * @return {PanZoom} The `PanZoom` instance.
         */
        function setPosition(x, y, z) {
            pz.pan.iPos = pz.pan.fPos;
            pz.transform = self.transform();
            pz.transform.x = x;
            pz.transform.y = y;
            if (typeof z === "number") {
                pz.zoom(z);
            } else {
                updateMatrix();
            }
            return pz;
        }

        /**
         * zoom
         * Zooms in/out the graph programatically.
         *
         * @name zoom
         * @function
         * @param {Number} z The zoom value which will be handled as `scale` internally.
         * @param {Number} oX An optional origin point `x` coordinate.
         * @param {Number} oY An optional origin point `y` coordinate.
         * @param {Event} oY An optional event value. If provided, the `zoom` event will be triggered.
         * @return {PanZoom} The `PanZoom` instance.
         */
        function zoom(z, oX, oY, ev) {

            // Handle zoom restrictions
            if (z > opt_options.zoom[1] || z < opt_options.zoom[0]) {
                return;
            }

            if (arguments.length === 1) {
                pz.transform = pz.transform || self.transform();
                pz.transform.scaleY = pz.transform.scaleX = z;
                updateMatrix();
                return pz;
            }

            var tr = pz.transform

                // Get the current x, y
              , currentX = tr.x
              , currentY = tr.y
              , scaleD = z / tr.scaleX

                // Compute the final x, y
              , x = scaleD * (currentX - oX) + oX
              , y = scaleD * (currentY - oY) + oY
              ;

            setPosition(x, y, z);

            if (ev) {
                self.node.dispatchEvent(new CustomEvent("zoom", { detail: { e: ev, tr: tr } }));
            }
        }

        // Pan zoom object
        var pz = {
            pan: {}
          , elm: self
          , setPosition: setPosition
          , zoom: zoom
        };

        // Set options
        opt_options = Object(opt_options);
        opt_options.zoom = opt_options.zoom || [];
        opt_options.zoomSpeed = typeof opt_options.zoomSpeed === "number" ? opt_options.zoomSpeed : -1;

        // Get the svg document
        var svg = self;
        while ((svg = self.parent()).node.tagName.toUpperCase() !== "SVG") {}

        // Create the rectangle
        var rect = new SVG(document.createDocumentFragment()).rect().attr({
            width: svg.width()
          , height: svg.height()
          , fill: "none"
        }).style("pointer-events", "all");

        // Insert the rectangle
        self.parent().node.insertBefore(rect.node, self.node);

        /*!
         * updateMatrix
         * An internal function called to update the svg matrix.
         *
         * @name updateMatrix
         * @function
         * @return {undefined}
         */
        function updateMatrix() {
            self.attr("transform", "matrix(" + [
                pz.transform.scaleX
              , 0, 0
              , pz.transform.scaleY
              , pz.transform.x
              , pz.transform.y
            ].join(",")+ ")");
        }

        /*!
         * pan
         * The internal function called for panning.
         *
         * @name pan
         * @function
         * @param {Event} e The internal listener event.
         * @return {undefined}
         */
        function pan(e) {
            if (!pz.pan.mousedown) {
                return;
            }
            var tr = pz.transform = self.transform();
            var diffX = pz.pan.fPos.x - pz.pan.iPos.x;
            var diffY = pz.pan.fPos.y - pz.pan.iPos.y;
            pz.setPosition(tr.x + diffX, tr.y + diffY);
            self.node.dispatchEvent(new CustomEvent("pan", { detail: { e: e, tr: tr } }));
        }

        /*!
         * mousePos
         * Returns the mouse point coordinates.
         *
         * @name mousePos
         * @function
         * @param {Event} e The mouse event.
         * @param {Boolean} rel If `true`, the relative coordinates will be returned instead.
         * @return {Object} An object containing the relative coordinates.
         */
        function mousePos(e, rel) {
            var bbox = self.parent().node.getBoundingClientRect()
              , abs = {
                    x: e.clientX || e.touches[0].pageX
                  , y: e.clientY || e.touches[0].pageY
                }
              ;
            if (!rel) { return abs; }
            return {
                x: abs.x - bbox.left
              , y: abs.y - bbox.top
            };
        }

        /*!
         * zoom
         * The internal function called for zooming.
         *
         * @name zoom
         * @function
         * @param {Event} e The internal listener event.
         * @return {undefined}
         */
        function doZoom (e) {

            // Get the relative mouse point
            var rP = mousePos(e, true)
              , oX = rP.x
              , oY = rP.y
              ;

            e.deltaY = e.deltaY || e.wheelDeltaY;

            // Compute the new scale
            var d = opt_options.zoomSpeed * e.deltaY / 1000
              , tr = pz.transform = self.transform()
              , scale = tr.scaleX + (tr.scaleX * d)
              ;

            zoom(scale, oX, oY, e);

            // Prevent the default browser behavior
            e.preventDefault();
        }

        // The event listeners
        var EventListeners = {
            mouse_down: function (e) {
                pz.pan.mousedown = true;
                pz.pan.iPos = mousePos(e);
            }
          , mouse_up: function (e) {
                pz.pan.mousedown = false;
                pz.pan.fPos = mousePos(e);
                pan();
            }
          , mouse_move: function (e) {
                if (!pz.pan.mousedown) { return; }
                pz.pan.fPos = mousePos(e);
                pan();
            }
          , mouse_leave: function (e) {
                pz.pan.mousedown = false;
            }
        };

        // Add event listeners
        rect
          .on(mousewheel, doZoom)
          .on("mousedown", EventListeners.mouse_down)
          .on("touchstart", EventListeners.mouse_down)
          .on("mousemove", EventListeners.mouse_move)
          .on("touchmove", EventListeners.mouse_move)
          .on("mouseup", EventListeners.mouse_up)
          .on("touchup", EventListeners.mouse_up)
          .on("mouseleave", EventListeners.mouse_leave)
          ;

        self.on(mousewheel, doZoom);
        return pz;
    }

    // Extend the SVG.Element with the new function
    SVG.extend(SVG.Element, {
        panZoom: panZoom
    });
}).call(this);
<|endoftext|>


<commit_before>'use strict';

define(['knockout', './string-hashtable'], function (ko, StringHashtable) {

    var requestAnimationFrame = window.requestAnimationFrame,
        cancelAnimationFrame = window.cancelAnimationFrame;

    function ElementWithBindingContext(element, bindingContext) {
        var self = this;

        self.element = element;
        self.bindingContext = bindingContext;
    }

    function AddedItem(index, item, id, previousId) {
        var self = this;

        self.index = index;
        self.item = item;
        self.id = id;
        self.previousId = previousId;
    }

    return function Synchronizer(configuration, bindingContext) {
        var self = this,
        // extract constants from the configuration
            idSelector = configuration.idSelector,
            itemVariableName = configuration.itemVariableName,
            indexVariableName = configuration.indexVariableName,
            allowDeviation = configuration.allowDeviation,
            reportDeviation = configuration.reportDeviation,
            reportSynchronization = configuration.reportSynchronization,
            parent = configuration.parent,
            startMarker = configuration.startMarker,
            endMarker = configuration.endMarker,
            itemElementTemplate = configuration.itemElementTemplate,
        // the core state -- when a synchronization is in progress, currentItems may be more current than itemElements
            currentItems = null,
            itemElements = new StringHashtable(), // id
            synchronizedCount = 0, // the number of items for which elements are present in the DOM (may deviate from itemElements.size())
        // state of the current synchronization (if one is in progress)
            step = 0, // synchronization step counter
            cursor = null, // cursor into the DOM for phase one and two (collectNewItemsAndMarkDeados/collectCarcasses)
            animationFrameRequest = null, // request for the next step, if synchronization is incremental
            addedItems = null, // items for which no element exists (yet)
            presumedDead = null, // elements for removed items by id, may contain false positives for moved back items
            carcasses = null; // elements for definitely removed items (no false positives)

        function startNewSynchronization(newItems) {
            step = 0;
            cursor = configuration.startMarker;
            addedItems = [];
            presumedDead = new StringHashtable();

            currentItems = newItems;

            if (allowDeviation)
                initiateIncrementalSynchronization();
            else
                performImmediateSynchronization();
        }

        function performImmediateSynchronization() {
            while (performSynchronizationStep())
                ++step;
            finalizeSynchronization();
        }

        function initiateIncrementalSynchronization() {
            performIncrementalSynchronizationStep(new Date().getTime() + 15); // TODO magic number, allow configuration?
        }

        function resumeIncrementalSynchronization() {
            performIncrementalSynchronizationStep(new Date().getTime() + 40); // TODO magic number, allow configuration?
        }

        function performIncrementalSynchronizationStep(timelimit) {
            while (performSynchronizationStep()) {
                ++step;

                if (new Date().getTime() > timelimit) {
                    reportDeviation();
                    animationFrameRequest = requestAnimationFrame(resumeIncrementalSynchronization);
                    return;
                }
            }

            finalizeSynchronization();
        }

        function performSynchronizationStep() {
            if (step < currentItems.length())
                collectNewItemsAndMarkDeados(step, currentItems.get(step));
            else if (!carcasses)
                collectCarcasses();
            else if (addedItems.length)
                if (configuration.allowElementRecycling && carcasses.length)
                    performNecromancy(carcasses.pop(), addedItems.shift());
                else
                    insertElementFor(addedItems.shift());
            else
                return incinerateCarcasses() && false;

            return true;
        }

        function collectNewItemsAndMarkDeados(index, item) {
            var previousId = index ? idSelector(currentItems.get(index - 1)) : null;
            var id = idSelector(item);
            var elementWithBindingContext = itemElements.get(id);

            if (elementWithBindingContext) {
                elementWithBindingContext.element.style.display = '';
                elementWithBindingContext.bindingContext[indexVariableName](index);

                var notDeadAfterAll = presumedDead.get(id);
                if (notDeadAfterAll) {
                    presumedDead.remove(id);
                    parent.insertBefore(notDeadAfterAll, cursor.nextSibling);
                    cursor = notDeadAfterAll;
                } else {
                    while (elementWithBindingContext.element !== cursor.nextSibling)
                        presumeDead(cursor.nextSibling);
                    cursor = elementWithBindingContext.element;
                }
            } else {
                addedItems.push(new AddedItem(index, item, id, previousId));
            }
        }

        function collectCarcasses() {
            carcasses = [];
            var aliveAndKicking = currentItems.length() - addedItems.length;
            for (var i = synchronizedCount - presumedDead.size - aliveAndKicking; i > 0; --i)
                presumeDead(cursor.nextSibling);
            presumedDead.forEach(function (key, element) {
                itemElements.remove(key);
                carcasses.push(element);
            });
            presumedDead.clear();
        }

        function performNecromancy(carcass, addedItem) {
            carcass.style.display = '';
            insertNodeAfter(carcass, addedItem.previousId);
            var revivedBindingContext = ko.contextFor(carcass);
            revivedBindingContext[itemVariableName](addedItem.item);
            revivedBindingContext[indexVariableName](addedItem.index);

            itemElements.add(addedItem.id, new ElementWithBindingContext(carcass, revivedBindingContext));
        }

        function insertElementFor(newborn) {
            var element = itemElementTemplate.cloneNode(true);

            var contextExtension = {};
            contextExtension[indexVariableName] = ko.observable(newborn.index);
            contextExtension[itemVariableName] = ko.observable(newborn.item);
            var newBindingContext = bindingContext.extend(contextExtension);

            itemElements.add(newborn.id, new ElementWithBindingContext(element, newBindingContext));
            ko.applyBindings(newBindingContext, element);
            insertNodeAfter(element, newborn.previousId);

            ++synchronizedCount;
        }

        function incinerateCarcasses() {
            while (carcasses.length)
                ko.removeNode(carcasses.pop());
        }

        function finalizeSynchronization() {
            for (var i = 0; i < carcasses.length; ++i) ko.removeNode(carcasses[i]);
            synchronizedCount = currentItems.length();
            reset();
            reportSynchronization();
        }

        function abortActiveSynchronization() {
            cancelAnimationFrame(animationFrameRequest);
            animationFrameRequest = null;

            for (var i = 0; carcasses !== null && i < carcasses.length; ++i) {
                var element = carcasses[i];
                itemElements.add(idFor(element), new ElementWithBindingContext(element, ko.contextFor(element)));
            }

            reset();
        }

        function reset() {
            step = 0;
            cursor = null;
            addedItems = null;
            presumedDead = null;
            carcasses = null;
        }

        function insertNodeAfter(node, previousId) {
            var before = (previousId ? itemElements.get(previousId).element : startMarker).nextSibling;
            parent.insertBefore(node, before);
        }

        function presumeDead(element) {
            presumedDead.add(idFor(element), element);
            element.style.display = 'none';
            parent.insertBefore(element, endMarker);
            return element;
        }

        function idFor(e) {
            return idSelector(ko.contextFor(e)[itemVariableName]());
        }

        self.startOrRestartSynchronization = function (newItems) {
            if (animationFrameRequest)
                abortActiveSynchronization();
            startNewSynchronization(newItems);
        };
        self.abortActiveSynchronization = abortActiveSynchronization;
    };
});<commit_msg>Minor refactor.<commit_after>'use strict';

define(['knockout', './string-hashtable'], function (ko, StringHashtable) {

    var requestAnimationFrame = window.requestAnimationFrame,
        cancelAnimationFrame = window.cancelAnimationFrame;

    function ElementWithBindingContext(element, bindingContext) {
        var self = this;

        self.element = element;
        self.bindingContext = bindingContext;
    }

    function AddedItem(index, item, id, previousId) {
        var self = this;

        self.index = index;
        self.item = item;
        self.id = id;
        self.previousId = previousId;
    }

    return function Synchronizer(configuration, bindingContext) {
        var self = this,
        // extract constants from the configuration
            idSelector = configuration.idSelector,
            itemVariableName = configuration.itemVariableName,
            indexVariableName = configuration.indexVariableName,
            allowDeviation = configuration.allowDeviation,
            reportDeviation = configuration.reportDeviation,
            reportSynchronization = configuration.reportSynchronization,
            parent = configuration.parent,
            startMarker = configuration.startMarker,
            endMarker = configuration.endMarker,
            itemElementTemplate = configuration.itemElementTemplate,
        // the core state -- when a synchronization is in progress, currentItems may be more current than itemElements
            currentItems = null,
            itemElements = new StringHashtable(), // id
            synchronizedCount = 0, // the number of items for which elements are present in the DOM (may deviate from itemElements.size())
        // state of the current synchronization (if one is in progress)
            step = 0, // synchronization step counter
            cursor = null, // cursor into the DOM for phase one and two (collectNewItemsAndMarkDeados/collectCarcasses)
            animationFrameRequest = null, // request for the next step, if synchronization is incremental
            addedItems = null, // items for which no element exists (yet)
            presumedDead = null, // elements for removed items by id, may contain false positives for moved back items
            carcasses = null; // elements for definitely removed items (no false positives)

        function startNewSynchronization(newItems) {
            step = 0;
            cursor = configuration.startMarker;
            addedItems = [];
            presumedDead = new StringHashtable();

            currentItems = newItems;

            if (allowDeviation)
                initiateIncrementalSynchronization();
            else
                performImmediateSynchronization();
        }

        function performImmediateSynchronization() {
            while (performSynchronizationStep())
                ++step;
            finalizeSynchronization();
        }

        function initiateIncrementalSynchronization() {
            performIncrementalSynchronizationStep(new Date().getTime() + 15); // TODO magic number, allow configuration?
        }

        function resumeIncrementalSynchronization() {
            performIncrementalSynchronizationStep(new Date().getTime() + 40); // TODO magic number, allow configuration?
        }

        function performIncrementalSynchronizationStep(timelimit) {
            while (performSynchronizationStep()) {
                ++step;

                if (new Date().getTime() > timelimit) {
                    reportDeviation();
                    animationFrameRequest = requestAnimationFrame(resumeIncrementalSynchronization);
                    return;
                }
            }

            finalizeSynchronization();
        }

        function performSynchronizationStep() {
            if (step < currentItems.length())
                collectNewItemsAndMarkDeados(step, currentItems.get(step));
            else if (!carcasses)
                collectCarcasses();
            else if (addedItems.length)
                if (configuration.allowElementRecycling && carcasses.length)
                    performNecromancy(carcasses.pop(), addedItems.shift());
                else
                    insertElementFor(addedItems.shift());
            else
                return incinerateCarcasses() && false;

            return true;
        }

        function collectNewItemsAndMarkDeados(index, item) {
            var previousId = index ? idSelector(currentItems.get(index - 1)) : null;
            var id = idSelector(item);
            var elementWithBindingContext = itemElements.get(id);

            if (elementWithBindingContext) {
                elementWithBindingContext.element.style.display = '';
                elementWithBindingContext.bindingContext[indexVariableName](index);

                var notDeadAfterAll = presumedDead.get(id);
                if (notDeadAfterAll) {
                    presumedDead.remove(id);
                    parent.insertBefore(notDeadAfterAll, cursor.nextSibling);
                    cursor = notDeadAfterAll;
                } else {
                    while (elementWithBindingContext.element !== cursor.nextSibling)
                        presumeDead(cursor.nextSibling);
                    cursor = elementWithBindingContext.element;
                }
            } else {
                addedItems.push(new AddedItem(index, item, id, previousId));
            }
        }

        function collectCarcasses() {
            carcasses = [];
            var aliveAndKicking = currentItems.length() - addedItems.length;
            for (var i = synchronizedCount - presumedDead.size - aliveAndKicking; i > 0; --i)
                presumeDead(cursor.nextSibling);
            presumedDead.forEach(function (key, element) {
                itemElements.remove(key);
                carcasses.push(element);
            });
            presumedDead.clear();
        }

        function performNecromancy(carcass, addedItem) {
            carcass.style.display = '';
            insertNodeAfter(carcass, addedItem.previousId);
            var revivedBindingContext = ko.contextFor(carcass);
            revivedBindingContext[itemVariableName](addedItem.item);
            revivedBindingContext[indexVariableName](addedItem.index);

            itemElements.add(addedItem.id, new ElementWithBindingContext(carcass, revivedBindingContext));
        }

        function insertElementFor(newborn) {
            var element = itemElementTemplate.cloneNode(true);

            var contextExtension = {};
            contextExtension[indexVariableName] = ko.observable(newborn.index);
            contextExtension[itemVariableName] = ko.observable(newborn.item);
            var newBindingContext = bindingContext.extend(contextExtension);

            itemElements.add(newborn.id, new ElementWithBindingContext(element, newBindingContext));
            ko.applyBindings(newBindingContext, element);
            insertNodeAfter(element, newborn.previousId);

            ++synchronizedCount;
        }

        function incinerateCarcasses() {
            while (carcasses.length)
                ko.removeNode(carcasses.pop());
        }

        function finalizeSynchronization() {
            for (var i = 0; i < carcasses.length; ++i) ko.removeNode(carcasses[i]);
            synchronizedCount = currentItems.length();
            reset();
            reportSynchronization();
        }

        function reset() {
            step = 0;
            cursor = null;
            addedItems = null;
            presumedDead = null;
            carcasses = null;
        }

        function insertNodeAfter(node, previousId) {
            var before = (previousId ? itemElements.get(previousId).element : startMarker).nextSibling;
            parent.insertBefore(node, before);
        }

        function presumeDead(element) {
            presumedDead.add(idFor(element), element);
            element.style.display = 'none';
            parent.insertBefore(element, endMarker);
            return element;
        }

        function idFor(e) {
            return idSelector(ko.contextFor(e)[itemVariableName]());
        }

        self.startOrRestartSynchronization = function (newItems) {
            abortActiveSynchronization();
            startNewSynchronization(newItems);
        };

        self.abortActiveSynchronization = abortActiveSynchronization;

        function abortActiveSynchronization() {
            if (!animationFrameRequest)
                return;

            cancelAnimationFrame(animationFrameRequest);
            animationFrameRequest = null;

            for (var i = 0; carcasses !== null && i < carcasses.length; ++i) {
                var element = carcasses[i];
                itemElements.add(idFor(element), new ElementWithBindingContext(element, ko.contextFor(element)));
            }

            reset();
        }
    };
});<|endoftext|>