MediaWiki:All.js

De Inkipedia
Revisión del 17:00 17 jul 2022 de Shahar (discusión | contribs.) (3D/2D Viewer)

Nota: Después de publicar, quizás necesite actualizar la caché de su navegador para ver los cambios.

  • Firefox/Safari: Mantenga presionada la tecla Shift mientras pulsa el botón Actualizar, o presiona Ctrl+F5 o Ctrl+R (⌘+R en Mac)
  • Google Chrome: presione Ctrl+Shift+R (⌘+Shift+R en Mac)
  • Internet Explorer/Edge: mantenga presionada Ctrl mientras pulsa Actualizar, o presione Ctrl+F5
  • Opera: Presiona Ctrl+F5.
/* Cualquier JavaScript aquí se cargará para todos los usuarios tanto en el escritorio como en el móvil */

/* strawpoll.me */
mw.loader.load('//splatoonwiki.org/wiki/MediaWiki:StrawpollIntegrator.js?action=raw&ctype=text/javascript')
/* strawpoll.com */
mw.loader.load('//splatoonwiki.org/wiki/MediaWiki:StrawpollIntegrator2.js?action=raw&ctype=text/javascript')

// ================================================================================
// JS/CSS específicos de la página
// ================================================================================

//Comprueba los archivos específicos de la página
$(function () {
    var url = new URL(window.location.href);
    var action = url.searchParams.get("action")
    if (action === null || action === "view" || action === "submit") {
        mw.loader.using("mediawiki.api", function () {
            var skin = mw.config.get("skin"),
                page = mw.config.get("wgPageName"),
                user = mw.config.get("wgUserName");

            var pages = [
                ['MediaWiki:Common.js/' + page + ".js", "js"],
                ['MediaWiki:Common.css/' + page + ".css", "css"],
                ['MediaWiki:' + skin + '.js/' + page + ".js", "js"],
                ['MediaWiki:' + skin + '.css/' + page + ".css", "css"]
            ];
            if (user != null) pages.push(
                ['User:' + user + '/common.js/' + page + ".js", "js"],
                ['User:' + user + '/common.css/' + page + ".css", "css"],
                ['User:' + user + '/' + skin + '.js/' + page + ".js", "js"],
                ['User:' + user + '/' + skin + '.css/' + page + ".css", "css"]
            );
            pages.forEach(function (el) {
                if (el[1] == "js") {
                    if (new URL(window.location).searchParams.get("disable-page-js") != null) return;
                    mw.loader.load('/w/index.php?title=' + encodeURIComponent(el[0]) + '&action=raw&ctype=text/javascript');
                }
                else {
                    if (new URL(window.location).searchParams.get("disable-page-css") != null) return;
                    mw.loader.load('/w/index.php?title=' + encodeURIComponent(el[0]) + '&action=raw&ctype=text/css', 'text/css');
                }
            });
        });
    }
});

// ================================================================================
// Función de sustitución del nombre de usuario para [[Template:USERNAME]]
// ================================================================================
// Inserta el nombre de usuario en <span class="insertusername"></span>.
// Desactivar estableciendo disableUsernameReplace = true.
jQuery(function($) {
  if (typeof(disableUsernameReplace) != 'undefined' && disableUsernameReplace)
    return;
  
  var username = mw.config.get('wgUserName');
  if (username == null)
    return;

  $('.insertusername').text(username);
});

// ================================================================================
// Función de sustitución de Editcount para [[Template:EDITCOUNT]]
// ================================================================================
// Inserta el recuento de ediciones en <span class="inserteditcount"></span>
jQuery(function($) {
  var userEditCount = mw.config.get('wgUserEditCount');
  if (userEditCount == null)
    return;

  $('.inserteditcount').text(userEditCount);
});

// ================================================================================
// Función de sustitución de la fecha de registro para [[Template:REGISTRATIONDATE]]
// ================================================================================
// Inserta la fecha de registro en <span class="insertregistrationdate"></span>
jQuery(function($) {
  var userRegistrationDate = mw.config.get('wgUserRegistration');
  if (userRegistrationDate == null)
    return;
  
  var d = new Date(0); // Sets the date to the epoch
  d.setUTCMilliseconds(userRegistrationDate);

  $('.insertregistrationdate').text(d.toLocaleString());
});

///////////////////////////////////////////////////////////////////////////////
//               Funciones de la utilidad de programación                    //
///////////////////////////////////////////////////////////////////////////////

// Avanza una marca de tiempo al siguiente múltiplo de 2 horas
function advanceDateTime(time) {
    var ret = new Date(time.getTime());
    ret.setMinutes(0);
    ret.setTime(ret.getTime() + 3600000 * (
        ret.getUTCHours() & 1 ? 1 : 2
    ));
    return ret;
}

// Formatea una marca de tiempo como una cadena en tiempo
function formatDateTime(time) {
    var ret =
        time.getDate()+ " " +
        [ "ene.", "feb.", "mar.", "abr.", "may.", "jun.",
          "jul.", "ago.", "sep.", "oct.", "nov.", "dic."
        ][time.getMonth()] + " (" +
        zeroPad(time.getHours(), 2) + ":" +
        zeroPad(time.getMinutes(), 2) + ")"
    ;
    return ret;
}

// Analiza una cadena de fecha UTC en el formato "MMM dd hh:mm YYYY", el año al final de la cadena es opcional y reemplaza el argumento del año si se proporciona
function parseDateTime(text, year) {
    text = text.split(/[\s:]+/);
    if(parseInt(text[4]) != NaN && parseInt(text[4]) < 9999 && parseInt(text[4]) >= 1970) year = text[4];
    return new Date(Date.UTC(
        year,
        { jan: 0, feb: 1, mar: 2, apr: 3, may:  4, jun:  5,
          jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11
        }[text[0].toLowerCase()],
        parseInt(text[1]), // Day
        parseInt(text[2]), // Hours
        parseInt(text[3]), // Minutes
        0, 0 // Seconds, milliseconds
    ));
}

// Convierte la última cadena obtenida en un objeto Date
function parseFetched(now, text) {
    var ret = parseDateTime(text, now.getUTCFullYear());
    if (now < ret) // Cuentas del límite del año
        ret.setUTCFullYear(ret.getUTCFullYear() - 1);
    return ret;
}

// Convierte una cadena de programación en un objeto
function parseSchedule(fetched, text) {
    var ret = parseDateTime(text, fetched.getUTCFullYear());
    if (ret.getTime() < fetched.getTime() - 8640000000)
        ret.setUTCFullYear(ret.getUTCFullYear() + 1);
    return ret;
}

// Calcula el tiempo restante hasta una marca de tiempo dada, como una cadena
function timeUntil(now, target) {
    target      = target.getTime() - now.getTime();
    target      = Math.floor(target % 7200000 / 1000);
    var seconds = zeroPad(target % 60, 2);
    var minutes = zeroPad(Math.floor(target / 60) % 60, 2);
    var hours   = Math.floor(target / 3600);
    return hours + ":" + minutes + ":" + seconds;
}

// Rellena un número con ceros a la izquierda
function zeroPad(number, digits) {
    number = "" + number;
    while (number.length < digits)
        number = "0" + number;
    return number;
}



///////////////////////////////////////////////////////////////////////////////
//                           Clase BattleSchedule                            //
///////////////////////////////////////////////////////////////////////////////

// Mantiene la actualización automática de los elementos del calendario de Ink Battle

// Constructor de objetos
var BattleSchedule = function() {

    // Inicializa los campos de la instancia
    this.lblNow     = document.getElementById("battle1");
    this.lblNext    = document.getElementById("battle2");
    this.lblFetched = document.getElementById("battleFetched");
    this.prev       = false;

    // Comprobación de errores
    if (!this.lblFetched) return; // No hay datos sobre el horario

    // Obtener las marcas de tiempo actuales y las últimas obtenidas
    var now     = new Date();
    var fetched = parseFetched(now, this.lblFetched.innerHTML);

    // Determina la marca de tiempo de las siguientes dos rotaciones
    this.next  = advanceDateTime(fetched);
    this.later = advanceDateTime(this.next);

    // Actualiza la visualización inicial
    this.onTick(now);
    this.lblFetched.innerHTML = formatDateTime(fetched);

    // Programa actualizaciones periódicas
    var that = this;
    this.timer = setInterval(function() { that.onTick(new Date()); }, 1000);
};

// Manipulador de actualizaciones periódicas
BattleSchedule.prototype.onTick = function(now) {

    // Manipulador de actualizaciones periódicas
    if (now >= this.next && !this.prevNow) {
        this.prev             = true;
        this.lblNow.innerHTML = "Anterior";
    }

    // Determina cuando la fila "Siguiente" entra en el pasado
    if (now >= this.later && !this.prevNext) {
        this.lblNext.innerHTML = "Anterior";
        clearInterval(this.timer);
        return;
    }

    // Muestra el tiempo hasta la siguiente rotación
    this.lblNext.innerHTML =
        (this.prev ? "Ahora, por otros " : "Siguiente, en ") +
        timeUntil(now, this.prev ? this.later : this.next)
    ;
};

new BattleSchedule();



///////////////////////////////////////////////////////////////////////////////
//                          Clase SalmonSchedule                             //
///////////////////////////////////////////////////////////////////////////////

// Mantiene la actualización automática de los elementos del calendario de Salmon Run

// Constructor de objetos
var SalmonSchedule = function() {

    // Obteiene las marcas de tiempo actuales y las últimas obtenidas
    var lblFetched = document.getElementById("salmonFetched");
    if (!lblFetched) return; // Sin horario
    var now        = new Date();
    var fetched    = parseFetched(now, lblFetched.innerHTML);

    // Inicializa los campos de la instancia
    this.slots = [
        this.parse(document.getElementById("salmon1"), fetched),
        this.parse(document.getElementById("salmon2"), fetched),
    ];

    // Actualiza la visualización inicial
    this.onTick(now);
    lblFetched.innerHTML = formatDateTime(fetched);

    // Programa actualizaciones periódicas
    var that = this;
    this.timer = setInterval(function() { that.onTick(new Date()); }, 1000);
};

// Manejador de actualización periódica
SalmonSchedule.prototype.onTick = function(now) {

    // Ciclo a través de las ranuras
    for (var x = 0; x < this.slots.length; x++) {
        var slot = this.slots[x];
        if (slot.prev) continue; // Saltar esta ranura

        // Determina cuándo esta ranura debe dejar de actualizarse
        slot.prev = now >= slot.end;

        // Actualiza el elemento
        slot.element.innerHTML =
            now >= slot.end ? "Anterior" :
            now >= slot.start ? "Ahora - " + formatDateTime(slot.end) :
            formatDateTime(slot.start) + " - " + formatDateTime(slot.end)
        ;
    }

    // Desprograma el temporizador
    if (this.slots[this.slots.length - 1].prev)
        clearInterval(this.timer);
};

// Analiza una única ranura de programación de Salmon Run
SalmonSchedule.prototype.parse = function(element, fetched) {
    var text = element.innerHTML;
    return {
        element: element,
        start:   parseSchedule(fetched, text.substring( 0, 12)),
        end:     parseSchedule(fetched, text.substring(15, 27)),
        prev:    false
    };
}

new SalmonSchedule();



///////////////////////////////////////////////////////////////////////////////
//                            Clase ShopSchedule                             //
///////////////////////////////////////////////////////////////////////////////

// Mantiene la actualización automática de los elementos del calendario de la tienda SplatNet 2

// Constructor de objetos
var ShopSchedule = function() {
    var lblFetched = document.getElementById("shopFetched");
    if (!lblFetched) return; // Sin horario

    // Obtiene las marcas de tiempo actuales y las últimas obtenidas
    var now     = new Date();
    var fetched = parseFetched(now, lblFetched.innerHTML);

    // Actualiza la visualización inicial
    lblFetched.innerHTML = formatDateTime(fetched);
};

new ShopSchedule();



///////////////////////////////////////////////////////////////////////////////
//                         Clase SplatfestSchedule                           //
///////////////////////////////////////////////////////////////////////////////

// Mantiene la actualización automática de los elementos del calendario del Splatfest

// Constructor de objetos
var SplatfestSchedule = function() {

    // Inicializa los campos de la instancia
    var now = new Date();
    this.slots = [
        this.parse(document.getElementById("splatfest1"), now),
        this.parse(document.getElementById("splatfest2"), now),
        this.parse(document.getElementById("splatfest3"), now)
    ];

    // Actualiza la visualización inicial
    this.onTick(now);

    // Programa actualizaciones periódicas
    var that = this;
    this.timer = setInterval(function() { that.onTick(new Date()); }, 1000);
};

// Manipulador de actualizaciones periódicas
SplatfestSchedule.prototype.onTick = function(now) {

    // Ciclo a través de las ranuras
    for (var x = 0; x < this.slots.length; x++) {
        var slot = this.slots[x];
        if (slot.prev) continue; // Saltar esta ranura

        // Determina cuando esta ranura debe dejar de actualizarse
        slot.prev = now >= slot.end;

        // Actualiza el elemento
        slot.element.innerHTML =
            now >= slot.end ? "Concluido" :
            now >= slot.start ? "Ahora - " + formatDateTime(slot.end) :
            formatDateTime(slot.start)
        ;
    }

    // Desprograma el temporizador
    if (this.slots[this.slots.length - 1].prev)
        clearInterval(this.timer);
};

// Analiza una sola ranura de Splatfest schdule
SplatfestSchedule.prototype.parse = function(element, now) {

    // Comprobación de errores
    if (!element) return { prev: true };

    // Determina la hora actual y las marcas de tiempo de inicio y fin
    var start = parseDateTime(element.innerHTML, now.getUTCFullYear());
    return {
        element: element,
        start:   start,
        end:     new Date(start.getTime() + 172800000),
        prev:    false
    };
};

new SplatfestSchedule();

// ================================================================================
// MediaLoader - Evitar que el audio se cargue hasta que se haga clic
// Version 2 (19.04.2020)
// ================================================================================

window.MediaLoader = {};
window.MediaLoader.FileCache = {};

function MLGetFileFromName(name){
    return new Promise(function(k,no){
        if(window.MediaLoader.FileCache[name] == null){
            new mw.Api().get({
                "action": "parse",
                "format": "json",
                "text": "[["+name+"]]",
                "prop": "text",
                "contentmodel": "wikitext"
            }).then(function(file){
                var filetext = $($.parseHTML(file.parse.text["*"])).find('p').html();
                window.MediaLoader.FileCache[name] = filetext;
                k(filetext);
            },no);
        }
        else
            k(window.MediaLoader.FileCache[name]);
    })
}

mw.loader.using("mediawiki.api", function(){
    $(".MediaLoader").each(function(){
        $(this).data("state", "unloaded");
        var children = $(this).children();
        if(children.length < 1){
            console.error("[MediaLoader] Error P1");
            return;
        }
        var child = $(children[0]);
        child.find(".MediaLoader-text").click(function(){
            var parent = $(this).parent().parent();
            try{
                if(parent.data("state") == "unloaded"){
                    parent.data("state", "busy");
                    $(this).text("Loading...");
                    
                    MLGetFileFromName(parent.data("file")).then(function(filetext){
                        parent.find(".MediaLoader-file").html(filetext);
                        parent.find(".MediaLoader-text").text("Unload "+parent.data("name"));
                        parent.data("state", "loaded");
                    }, console.error)
                }
                else if(parent.data("state") == "loaded"){
                    parent.find(".MediaLoader-file").html("");
                    $(this).text("Load "+parent.data("name"));
                    parent.data("state", "unloaded");
                }
            }
            catch(ex){
                console.error(ex);
                parent.data("state", "error");
                $(this).text("An unexpected error has occured");
                parent.find(".MediaLoader-file").html("<a></a>");
                parent.find(".MediaLoader-file").children("a").attr("href", "//splatoonwiki.org/wiki/"+parent.data("file"))
                parent.find(".MediaLoader-file").children("a").text(parent.data("name"))
                $(this).css("cursor", "");
            }
        })
        child.find(".MediaLoader-text").text("Load "+$(this).data("name"));
        child.find(".MediaLoader-text").addClass("noselect");
        child.find(".MediaLoader-text").css("cursor", "pointer");
        child.find(".MediaLoader-file").addClass("noselect");
    })
    
    $(".MediaLoadAll").each(function(){
        var children = $(this).children();
        if(children.length < 2){
            console.error("[MediaLoadAll] Error P1");
            return;
        }
        children.click(function(){
            var parent = $(this).parent();
            try{
                var load = $(this).hasClass("MediaLoadAll-load");
                $(parent.data("group") != "{{{group}}}" ? '.MediaLoader[data-group="'+parent.data("group")+'"]' : ".MediaLoader").each(function(){
                    if(($(this).data("state") == "unloaded" && load) || ($(this).data("state") == "loaded" && !load))
                        $(this).find(".MediaLoader-text").click();
                })
            }
            catch(ex){
                console.error(ex);
                $(this).text("An unexpected error has occured");
                $(this).css("cursor", "");
            }
        })
        $(this).css("display", "")
        children.filter(".MediaLoadAll-load").text("Load all "+$(this).data("name"));
        children.filter(".MediaLoadAll-unload").text("Unload all "+$(this).data("name"));
        children.addClass("noselect");
        children.css("cursor", "pointer");
    })
})

// ================================================================================
// Countdowns
// ================================================================================
// Credits go to AbelToy, Guy Perfect, Espyo for the countdown code.

// List of countdowns on the current page
var countdowns = [];

// Converts from time to a clean time info structure
function timeToStruct(time) {

  var passed = time < 0; //Has the moment passed?
  
  // Parse time fields from the number
                                              time = Math.floor(time / 1000);
  var secs  = ("00" + (time % 60)).slice(-2); time = Math.floor(time /   60);
  var mins  = ("00" + (time % 60)).slice(-2); time = Math.floor(time /   60);
  var hours = ("00" + (time % 24)).slice(-2); time = Math.floor(time /   24);

  // Construct the string representation
  return {
    d: time,
    h: hours,
    m: mins,
    s: secs,
    p: passed
  };
  
}

// Gets the time remaining until the next stage rotation
function getStageCountdown(now) {
  var hour   = Math.floor(now / 3600000) % 24 + 2; // Add 2 for UTC bias
  var now    = hour * 3600000 + now % 3600000;     // Current adjusted hour
  var target = (hour + 4 & -4) * 3600000;          // Target hour
  return target - now;
}

function tickCountdowns() {
  var now = Date.now();
  
  for(var c = 0; c < countdowns.length; c++){
    
    var diff = 0;
    if(countdowns[c].stage) {
      diff = timeToStruct(getStageCountdown(now));
    } else {
      diff = timeToStruct(countdowns[c].time - now);
    }
    
    if(diff.p && diff.d < -1) {
      // Over 24 hours passed
      countdowns[c].span.innerHTML = countdowns[c].doneMessage;
    } else if(diff.p){
      // 24 hours haven't passed yet
      countdowns[c].span.innerHTML = countdowns[c].ongoingMessage;
    } else {
      // The time hasn't come yet
      countdowns[c].span.innerHTML =
        diff.d + "d :" +
        diff.h + "h :" +
        diff.m + "m :" +
        diff.s + "s";
    }
  }
}

// Returns the info from a countdown span on the page.
function getCountdownInfo(countdown, stage) {
  var time = null;
  var ongoingMessage = "";
  var doneMessage = "";

  if(!stage) {
    // Format is "<day> <hour>|<24 hour msg>|<afterwards msg>"
    var parts = countdown.innerHTML.split("|");
    doneMessage = (parts.length >= 3) ? parts[2] : parts[1];
    ongoingMessage = parts[1];
    
    var timeParts = parts[0].split(/[ \n]/);
    var date = timeParts[0].split("/");
    var hour = timeParts[1].split(":");
    time = Date.UTC(date[0], date[1] - 1, date[2], hour[0], hour[1]);
  }
  
  countdowns.push( {
    span:           countdown,
    stage:          stage,
    time:           time,
    ongoingMessage: ongoingMessage,
    doneMessage:    doneMessage
  } );
  
  // The spans start hidden and with the info
  // Delete the info and show the span
  countdown.style.display = "inline";
  countdown.innerHTML = "";
}

// Finds countdown spans on the document and sets up the countdowns
function setupCountdowns() {
  var stageCountdowns = document.getElementsByClassName("stageCountdown");
  for(var sc = 0; sc < stageCountdowns.length; sc++) {
    getCountdownInfo(stageCountdowns[sc], true);
  }
  
  var countdowns = document.getElementsByClassName("countdown");
  for(var c = 0; c < countdowns.length; c++) {
    getCountdownInfo(countdowns[c], false);
  }
  
  setInterval(tickCountdowns, 1000);
}

setupCountdowns();

// ================================================================================
// 3D/2D Viewer
// ================================================================================

// 3D model viewer
var viewer3d = {
	dragging: null,
	draggingFrameX: 0,
	draggingFrameY: 0,
	viewers: [],
	frameThresholdX: 10,
	frameThresholdY: 128,
	realMod: function(x, y) {
		return ((x % y) + y) % y;
	},
	init: function() {
		$('.viewer-3d').each(viewer3d.bind);
		$(document).mouseup(viewer3d.release);
		$(document).mousemove(viewer3d.move);
	},
	bind: function() {
		var v = $(this);
		var num = viewer3d.viewers.length;
		var allModels = [];
		var modelID = 0;
		var viewerSize = 0;
		while(true) {
			var modelMap = v.find('.viewer-3d-map-' + modelID);
			var urlNode = v.find('.viewer-3d-url-' + modelID);
			if(!modelMap.length || !urlNode.length) break;
			var url = $('<div/>').html(urlNode.text()).text();
			var framesS = $('<div/>').html(modelMap.text()).text().replace(/^\s+|\s+$/g).split(/,/g);
			var frameMap = [];
			var heightMap = [];
			var leftCropMap = [];
			var totalW = parseInt(framesS[0]);
			var maxFrameW = parseInt(framesS[1]);
			var totalH = parseInt(framesS[2]);
			var verticalSteps = parseInt(framesS[3]);
			var midVertical = Math.floor(verticalSteps / 2);
			for(var f = 4; f < framesS.length; f += 3) {
				frameMap.push(parseInt(framesS[f]));
				heightMap.push(parseInt(framesS[f + 1]));
				leftCropMap.push(parseInt(framesS[f + 2]));
			}
			allModels.push({
				imageURL: url,
				map: frameMap,
				cropMap: leftCropMap,
				totalWidth: totalW,
				totalHeight: totalH,
				maxFrameWidth: maxFrameW,
				xStep: verticalSteps
			});
			viewerSize = Math.max(viewerSize, totalH, maxFrameW);
			modelID++;
		}
		if(!modelID) return;
		var overlayNode = $('<div class="viewer-3d-overlay"></div>');
		var frameN = v.find('.viewer-3d-frame');
		v.find('img').detach();
		var klasses = v.attr('class').split(/ /g);
		var startFrame = 0;
		for(var k in klasses) {
			if(klasses[k].substr(0, 11) == 'startframe-') {
				startFrame = Math.max(0, parseInt(klasses[k].substr(11)));
			}
		}
		var viewer = {
			node: v,
			frameX: startFrame,
			frameY: midVertical,
			models: allModels,
			currentModel: -1,
			frameNode: frameN,
			width: viewerSize,
			height: viewerSize,
			mouseX: 0,
			mouseY: 0,
			overlay: overlayNode
		};
		viewer3d.viewers.push(viewer);
		v.hover(viewer3d.hover, viewer3d.unhover).mousedown(viewer3d.drag).append(overlayNode).attr('data-id', num).css({
			width: viewer.width + 'px',
			height: viewer.height + 'px'
		});
		frameN.mousedown(viewer3d.drag).attr('data-id', num).css('height', viewer.height + 'px');
		viewer3d.changeVersion(viewer, 0);
	},
	getCurrentModel: function(v) {
		return v.models[v.currentModel];
	},
	changeVersion: function(v, version) {
		version = Math.max(0, Math.min(v.models.length - 1, version));
		if(v.currentModel == version) return;
		v.currentModel = version;
		v.frameNode.css('background', 'url(' + viewer3d.getCurrentModel(v).imageURL + ') top left no-repeat');
		viewer3d.display(v, v.frameX, v.frameY);
	},
	hover: function(e) {
		var v = viewer3d.getViewer(this);
		if(viewer3d.dragging != v) {
			v.overlay.animate({'opacity': '1'}, 'fast');
		}
	},
	unhover: function(e) {
		var v = viewer3d.getViewer(this);
		if(viewer3d.dragging != v) {
			v.overlay.animate({'opacity': '0.5'}, 'fast');
		}
	},
	drag: function(e) {
		var v = viewer3d.getViewer(this);
		v.mouseX = e.pageX;
		v.mouseY = e.pageY;
		viewer3d.dragging = v;
		draggingFrameX = v.frameX;
		draggingFrameY = v.frameY;
		return false;
	},
	release: function() {
		var v = viewer3d.dragging;
		viewer3d.dragging = null;
		if(v != null) {
			v.frameX = viewer3d.draggingFrameX;
			v.frameY = viewer3d.draggingFrameY;
			v.overlay.animate({'opacity': '0.5'}, 'fast');
		}
		viewer3d.draggingFrameX = 0;
		viewer3d.draggingFrameY = 0;
	},
	getViewer: function(node) {
		return viewer3d.viewers[parseInt($(node).attr('data-id'))];
	},
	display: function(v, frameX, frameY) {
		var model = viewer3d.getCurrentModel(v);
		var frameID = viewer3d.realMod(frameX * model.xStep + frameY, model.map.length);
		var frameOffset = model.map[frameID];
		var frameWidth = 0;
		if(frameID == model.map.length - 1) {
			frameWidth = model.totalWidth - frameOffset;
		} else {
			frameWidth = model.map[frameID + 1] - frameOffset;
		}
		v.frameNode.css({
			backgroundPosition: (-frameOffset - frameID) + 'px 0px',
			left: Math.round((v.width - model.maxFrameWidth) / 2.0 + model.cropMap[frameID]) + 'px',
			top: Math.round((v.height - model.totalHeight) / 2) + 'px',
			width: frameWidth + 'px',
			height: model.totalHeight + 'px'
		});
	},
	move: function(e) {
		if(viewer3d.dragging == null) {
			return;
		}
		var v = viewer3d.dragging;
		var model = viewer3d.getCurrentModel(v);
		var mouseDeltaX = e.pageX - v.mouseX;
		var mouseDeltaY = e.pageY - v.mouseY;
		var frameDeltaX = Math.round(mouseDeltaX / viewer3d.frameThresholdX);
		var frameDeltaY = -Math.round(mouseDeltaY / viewer3d.frameThresholdY);
		viewer3d.draggingFrameX = v.frameX + frameDeltaX;
		viewer3d.draggingFrameY = Math.max(0, Math.min(model.xStep - 1, v.frameY + frameDeltaY));
		viewer3d.display(v, viewer3d.draggingFrameX, viewer3d.draggingFrameY);
	}
};
$(viewer3d.init);
var selector3d = {
	bind: function() {
		var viewer = viewer3d.getViewer($(this).find('.viewer-3d'));
		var keepGoing = true;
		var modelVariant = 0;
		var selector;
		while(keepGoing) {
			selector = $(this).find('.selector-' + modelVariant);
			if(selector.length) {
				selector.attr('data-variant', modelVariant).click(function() {
					viewer3d.changeVersion(viewer, parseInt($(this).attr('data-variant')));
					return false;
				});
			}
			modelVariant++;
			keepGoing = selector.length;
		}
	},
	init: function() {
		$('.viewer-3d-multi, .viewer-3d-container').each(selector3d.bind);
	}
};
$(selector3d.init);

// Code to get 3D viewer drag working on touch devices
// Source: http://www.jquery4u.com/mobile/jquery-add-dragtouch-support-ipad/
$.fn.addTouch = function(){
    this.each(function(i,el){
      $(el).bind('touchstart touchmove touchend touchcancel',function(){
        //we pass the original event object because the jQuery event
        //object is normalized to w3c specs and does not provide the TouchList
        handleTouch(event);
      });
    });

    var handleTouch = function(event)
    {
      var touches = event.changedTouches,
              first = touches[0],
              type = '';

      switch(event.type)
      {
        case 'touchstart':
          type = 'mousedown';
          break;

        case 'touchmove':
          type = 'mousemove';
          event.preventDefault();
          break;

        case 'touchend':
          type = 'mouseup';
          break;

        default:
          return;
      }

      var simulatedEvent = document.createEvent('MouseEvent');
      simulatedEvent.initMouseEvent(type, true, true, window, 1, first.screenX, first.screenY, first.clientX, first.clientY, false, false, false, false, 0/*left*/, null);
      first.target.dispatchEvent(simulatedEvent);
    };
  };

$('.viewer-3d').addTouch();

// End 3D viewer touch device code