var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var H5P = H5P || {}; /** * H5P-Timer * * General purpose timer that can be used by other H5P libraries. * * @param {H5P.jQuery} $ */ H5P.Timer = function ($, EventDispatcher) { /** * Create a timer. * * @constructor * @param {number} [interval=Timer.DEFAULT_INTERVAL] - The update interval. */ function Timer() { var interval = arguments.length <= 0 || arguments[0] === undefined ? Timer.DEFAULT_INTERVAL : arguments[0]; var self = this; // time on clock and the time the clock has run var clockTimeMilliSeconds = 0; var playingTimeMilliSeconds = 0; // used to update recurring notifications var clockUpdateMilliSeconds = 0; // indicators for total running time of the timer var firstDate = null; var startDate = null; var lastDate = null; // update loop var loop = null; // timer status var status = Timer.STOPPED; // indicate counting direction var mode = Timer.FORWARD; // notifications var notifications = []; // counter for notifications; var notificationsIdCounter = 0; // Inheritance H5P.EventDispatcher.call(self); // sanitize interval if (Timer.isInteger(interval)) { interval = Math.max(interval, 1); } else { interval = Timer.DEFAULT_INTERVAL; } /** * Get the timer status. * * @public * @return {number} The timer status. */ self.getStatus = function () { return status; }; /** * Get the timer mode. * * @public * @return {number} The timer mode. */ self.getMode = function () { return mode; }; /** * Get the time that's on the clock. * * @private * @return {number} The time on the clock. */ var getClockTime = function getClockTime() { return clockTimeMilliSeconds; }; /** * Get the time the timer was playing so far. * * @private * @return {number} The time played. */ var getPlayingTime = function getPlayingTime() { return playingTimeMilliSeconds; }; /** * Get the total running time from play() until stop(). * * @private * @return {number} The total running time. */ var getRunningTime = function getRunningTime() { if (!firstDate) { return 0; } if (status !== Timer.STOPPED) { return new Date().getTime() - firstDate.getTime(); } else { return !lastDate ? 0 : lastDate.getTime() - firstDate; } }; /** * Get one of the times. * * @public * @param {number} [type=Timer.TYPE_CLOCK] - Type of the time to get. * @return {number} Clock Time, Playing Time or Running Time. */ self.getTime = function () { var type = arguments.length <= 0 || arguments[0] === undefined ? Timer.TYPE_CLOCK : arguments[0]; if (!Timer.isInteger(type)) { return; } // break will never be reached, but for consistency... switch (type) { case Timer.TYPE_CLOCK: return getClockTime(); break; case Timer.TYPE_PLAYING: return getPlayingTime(); break; case Timer.TYPE_RUNNING: return getRunningTime(); break; default: return getClockTime(); } }; /** * Set the clock time. * * @public * @param {number} time - The time in milliseconds. */ self.setClockTime = function (time) { if ($.type(time) === 'string') { time = Timer.toMilliseconds(time); } if (!Timer.isInteger(time)) { return; } // notifications only need an update if changing clock against direction clockUpdateMilliSeconds = (time - clockTimeMilliSeconds) * mode < 0 ? time - clockTimeMilliSeconds : 0; clockTimeMilliSeconds = time; }; /** * Reset the timer. * * @public */ self.reset = function () { if (status !== Timer.STOPPED) { return; } clockTimeMilliSeconds = 0; playingTimeMilliSeconds = 0; firstDate = null; lastDate = null; loop = null; notifications = []; notificationsIdCounter = 0; self.trigger('reset', {}, {bubbles: true, external: true}); }; /** * Set timer mode. * * @public * @param {number} mode - The timer mode. */ self.setMode = function (direction) { if (direction !== Timer.FORWARD && direction !== Timer.BACKWARD) { return; } mode = direction; }; /** * Start the timer. * * @public */ self.play = function () { if (status === Timer.PLAYING) { return; } if (!firstDate) { firstDate = new Date(); } startDate = new Date(); status = Timer.PLAYING; self.trigger('play', {}, {bubbles: true, external: true}); update(); }; /** * Pause the timer. * * @public */ self.pause = function () { if (status !== Timer.PLAYING) { return; } status = Timer.PAUSED; self.trigger('pause', {}, {bubbles: true, external: true}); }; /** * Stop the timer. * * @public */ self.stop = function () { if (status === Timer.STOPPED) { return; } lastDate = new Date(); status = Timer.STOPPED; self.trigger('stop', {}, {bubbles: true, external: true}); }; /** * Update the timer until Timer.STOPPED. * * @private */ var update = function update() { var currentMilliSeconds = 0; // stop because requested if (status === Timer.STOPPED) { clearTimeout(loop); return; } //stop because countdown reaches 0 if (mode === Timer.BACKWARD && clockTimeMilliSeconds <= 0) { self.stop(); return; } // update times if (status === Timer.PLAYING) { currentMilliSeconds = new Date().getTime() - startDate; clockTimeMilliSeconds += currentMilliSeconds * mode; playingTimeMilliSeconds += currentMilliSeconds; } startDate = new Date(); checkNotifications(); loop = setTimeout(function () { update(); }, interval); }; /** * Get next notification id. * * @private * @return {number} id - The next id. */ var getNextNotificationId = function getNextNotificationId() { return notificationsIdCounter++; }; /** * Set a notification * * @public * @param {Object|String} params - Parameters for the notification. * @callback callback - Callback function. * @return {number} ID of the notification. */ self.notify = function (params, callback) { var id = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : getNextNotificationId(); // common default values for the clock timer // TODO: find a better place for this, maybe a JSON file? var defaults = {}; defaults['every_tenth_second'] = { "repeat": 100 }; defaults['every_second'] = { "repeat": 1000 }; defaults['every_minute'] = { "repeat": 60000 }; defaults['every_hour'] = { "repeat": 3600000 }; // Sanity check for callback function if (!callback instanceof Function) { return; } // Get default values if ($.type(params) === 'string') { params = defaults[params]; } if (params !== null && (typeof params === 'undefined' ? 'undefined' : _typeof(params)) === 'object') { // Sanitize type if (!params.type) { params.type = Timer.TYPE_CLOCK; } else { if (!Timer.isInteger(params.type)) { return; } if (params.type < Timer.TYPE_CLOCK || params.type > Timer.TYPE_RUNNING) { return; } } // Sanitize mode if (!params.mode) { params.mode = Timer.NOTIFY_ABSOLUTE; } else { if (!Timer.isInteger(params.mode)) { return; } if (params.mode < Timer.NOTIFY_ABSOLUTE || params.type > Timer.NOTIFY_RELATIVE) { return; } } // Sanitize calltime if (!params.calltime) { params.calltime = params.mode === Timer.NOTIFY_ABSOLUTE ? self.getTime(params.type) : 0; } else { if ($.type(params.calltime) === 'string') { params.calltime = Timer.toMilliseconds(params.calltime); } if (!Timer.isInteger(params.calltime)) { return; } if (params.calltime < 0) { return; } if (params.mode === Timer.NOTIFY_RELATIVE) { params.calltime = Math.max(params.calltime, interval); if (params.type === Timer.TYPE_CLOCK) { // clock could be running backwards params.calltime *= mode; } params.calltime += self.getTime(params.type); } } // Sanitize repeat if ($.type(params.repeat) === 'string') { params.repeat = Timer.toMilliseconds(params.repeat); } // repeat must be >= interval (ideally multiple of interval) if (params.repeat !== undefined) { if (!Timer.isInteger(params.repeat)) { return; } params.repeat = Math.max(params.repeat, interval); } } else { // neither object nor string return; } // add notification notifications.push({ 'id': id, 'type': params.type, 'calltime': params.calltime, 'repeat': params.repeat, 'callback': callback }); return id; }; /** * Remove a notification. * * @public * @param {number} id - The id of the notification. */ self.clearNotification = function (id) { notifications = $.grep(notifications, function (item) { return item.id === id; }, true); }; /** * Set a new starting time for notifications. * * @private * @param elements {Object] elements - The notifications to be updated. * @param deltaMilliSeconds {Number} - The time difference to be set. */ var updateNotificationTime = function updateNotificationTime(elements, deltaMilliSeconds) { if (!Timer.isInteger(deltaMilliSeconds)) { return; } elements.forEach(function (element) { // remove notification self.clearNotification(element.id); //rebuild notification with new data self.notify({ 'type': element.type, 'calltime': self.getTime(element.type) + deltaMilliSeconds, 'repeat': element.repeat }, element.callback, element.id); }); }; /** * Check notifications for necessary callbacks. * * @private */ var checkNotifications = function checkNotifications() { var backwards = 1; var elements = []; // update recurring clock notifications if clock was changed if (clockUpdateMilliSeconds !== 0) { elements = $.grep(notifications, function (item) { return item.type === Timer.TYPE_CLOCK && item.repeat != undefined; }); updateNotificationTime(elements, clockUpdateMilliSeconds); clockUpdateMilliSeconds = 0; } // check all notifications for triggering notifications.forEach(function (element) { /* * trigger if notification time is in the past * which means calltime >= Clock Time if mode is BACKWARD (= -1) */ backwards = element.type === Timer.TYPE_CLOCK ? mode : 1; if (element.calltime * backwards <= self.getTime(element.type) * backwards) { // notify callback function element.callback.apply(this); // remove notification self.clearNotification(element.id); // You could use updateNotificationTime() here, but waste some time // rebuild notification if it should be repeated if (element.repeat) { self.notify({ 'type': element.type, 'calltime': self.getTime(element.type) + element.repeat * backwards, 'repeat': element.repeat }, element.callback, element.id); } } }); }; } // Inheritance Timer.prototype = Object.create(H5P.EventDispatcher.prototype); Timer.prototype.constructor = Timer; /** * Generate timecode elements from milliseconds. * * @private * @param {number} milliSeconds - The milliseconds. * @return {Object} The timecode elements. */ var toTimecodeElements = function toTimecodeElements(milliSeconds) { var years = 0; var month = 0; var weeks = 0; var days = 0; var hours = 0; var minutes = 0; var seconds = 0; var tenthSeconds = 0; if (!Timer.isInteger(milliSeconds)) { return; } milliSeconds = Math.round(milliSeconds / 100); tenthSeconds = milliSeconds - Math.floor(milliSeconds / 10) * 10; seconds = Math.floor(milliSeconds / 10); minutes = Math.floor(seconds / 60); hours = Math.floor(minutes / 60); days = Math.floor(hours / 24); weeks = Math.floor(days / 7); month = Math.floor(days / 30.4375); // roughly (30.4375 = mean of 4 years) years = Math.floor(days / 365); // roughly (no leap years considered) return { years: years, month: month, weeks: weeks, days: days, hours: hours, minutes: minutes, seconds: seconds, tenthSeconds: tenthSeconds }; }; /** * Extract humanized time element from time for concatenating. * * @public * @param {number} milliSeconds - The milliSeconds. * @param {string} element - Time element: hours, minutes, seconds or tenthSeconds. * @param {boolean} [rounded=false] - If true, element value will be rounded. * @return {number} The time element. */ Timer.extractTimeElement = function (time, element) { var rounded = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2]; var timeElements = null; if ($.type(time) === 'string') { time = Timer.toMilliseconds(time); } if (!Timer.isInteger(time)) { return; } if ($.type(element) !== 'string') { return; } if ($.type(rounded) !== 'boolean') { return; } if (rounded) { timeElements = { years: Math.round(time / 31536000000), month: Math.round(time / 2629800000), weeks: Math.round(time / 604800000), days: Math.round(time / 86400000), hours: Math.round(time / 3600000), minutes: Math.round(time / 60000), seconds: Math.round(time / 1000), tenthSeconds: Math.round(time / 100) }; } else { timeElements = toTimecodeElements(time); } return timeElements[element]; }; /** * Convert time in milliseconds to timecode. * * @public * @param {number} milliSeconds - The time in milliSeconds. * @return {string} The humanized timecode. */ Timer.toTimecode = function (milliSeconds) { var timecodeElements = null; var timecode = ''; var minutes = 0; var seconds = 0; if (!Timer.isInteger(milliSeconds)) { return; } if (milliSeconds < 0) { return; } timecodeElements = toTimecodeElements(milliSeconds); minutes = Math.floor(timecodeElements['minutes'] % 60); seconds = Math.floor(timecodeElements['seconds'] % 60); // create timecode if (timecodeElements['hours'] > 0) { timecode += timecodeElements['hours'] + ':'; } if (minutes < 10) { timecode += '0'; } timecode += minutes + ':'; if (seconds < 10) { timecode += '0'; } timecode += seconds + '.'; timecode += timecodeElements['tenthSeconds']; return timecode; }; /** * Convert timecode to milliseconds. * * @public * @param {string} timecode - The timecode. * @return {number} Milliseconds derived from timecode */ Timer.toMilliseconds = function (timecode) { var head = []; var tail = ''; var hours = 0; var minutes = 0; var seconds = 0; var tenthSeconds = 0; if (!Timer.isTimecode(timecode)) { return; } // thx to the regexp we know everything can be converted to a legit integer in range head = timecode.split('.')[0].split(':'); while (head.length < 3) { head = ['0'].concat(head); } hours = parseInt(head[0]); minutes = parseInt(head[1]); seconds = parseInt(head[2]); tail = timecode.split('.')[1]; if (tail) { tenthSeconds = Math.round(parseInt(tail) / Math.pow(10, tail.length - 1)); } return (hours * 36000 + minutes * 600 + seconds * 10 + tenthSeconds) * 100; }; /** * Check if a string is a timecode. * * @public * @param {string} value - String to check * @return {boolean} true, if string is a timecode */ Timer.isTimecode = function (value) { var reg_timecode = /((((((\d+:)?([0-5]))?\d:)?([0-5]))?\d)(\.\d+)?)/; if ($.type(value) !== 'string') { return false; } return value === value.match(reg_timecode)[0] ? true : false; }; // Workaround for IE and potentially other browsers within Timer object Timer.isInteger = Timer.isInteger || function(value) { return typeof value === "number" && isFinite(value) && Math.floor(value) === value; }; // Timer states /** @constant {number} */ Timer.STOPPED = 0; /** @constant {number} */ Timer.PLAYING = 1; /** @constant {number} */ Timer.PAUSED = 2; // Timer directions /** @constant {number} */ Timer.FORWARD = 1; /** @constant {number} */ Timer.BACKWARD = -1; /** @constant {number} */ Timer.DEFAULT_INTERVAL = 10; // Counter types /** @constant {number} */ Timer.TYPE_CLOCK = 0; /** @constant {number} */ Timer.TYPE_PLAYING = 1; /** @constant {number} */ Timer.TYPE_RUNNING = 2; // Notification types /** @constant {number} */ Timer.NOTIFY_ABSOLUTE = 0; /** @constant {number} */ Timer.NOTIFY_RELATIVE = 1; return Timer; }(H5P.jQuery, H5P.EventDispatcher); ; H5P.MemoryGame = (function (EventDispatcher, $) { // We don't want to go smaller than 100px per card(including the required margin) var CARD_MIN_SIZE = 100; // PX var CARD_STD_SIZE = 116; // PX var STD_FONT_SIZE = 16; // PX var LIST_PADDING = 1; // EMs var numInstances = 0; /** * Memory Game Constructor * * @class H5P.MemoryGame * @extends H5P.EventDispatcher * @param {Object} parameters * @param {Number} id * @param {Object} [extras] Saved state, metadata, etc. * @param {object} [extras.previousState] The previous state of the game */ function MemoryGame(parameters, id, extras) { /** @alias H5P.MemoryGame# */ var self = this; this.previousState = extras.previousState ?? {}; // Initialize event inheritance EventDispatcher.call(self); var flipped, timer, counter, popup, $bottom, $feedback, $wrapper, maxWidth, numCols, audioCard; var cards = []; var score = 0; numInstances++; // Add defaults parameters = $.extend(true, { l10n: { cardTurns: 'Card turns', timeSpent: 'Time spent', feedback: 'Good work!', tryAgain: 'Reset', closeLabel: 'Close', label: 'Memory Game. Find the matching cards.', labelInstructions: 'Use arrow keys left and right to navigate cards. Use space or enter key to turn card.', done: 'All of the cards have been found.', cardPrefix: 'Card %num of %total:', cardUnturned: 'Unturned. Click to turn.', cardTurned: 'Turned.', cardMatched: 'Match found.', cardMatchedA11y: 'Your cards match!', cardNotMatchedA11y: 'Your chosen cards do not match. Turn other cards to try again.' } }, parameters); // Filter out invalid cards parameters.cards = (parameters.cards ?? []).filter((cardParams) => { return MemoryGame.Card.isValid(cardParams); }); /** * Get number of cards that are currently flipped and in game. * @returns {number} Number of cards that are currently flipped. */ var getNumFlipped = () => { return cards .filter((card) => card.isFlipped() && !card.isRemoved()) .length; }; /** * Check if these two cards belongs together. * * @private * @param {H5P.MemoryGame.Card} card * @param {H5P.MemoryGame.Card} mate * @param {H5P.MemoryGame.Card} correct */ var check = function (card, mate, correct) { if (mate !== correct) { ariaLiveRegion.read(parameters.l10n.cardNotMatchedA11y); return; } // Remove them from the game. card.remove(); mate.remove(); var isFinished = cards.every((card) => card.isRemoved()); var desc = card.getDescription(); if (desc !== undefined) { // Pause timer and show desciption. timer.pause(); var imgs = [card.getImage()]; if (card.hasTwoImages) { imgs.push(mate.getImage()); } // Keep message for dialog modal shorter without instructions $applicationLabel.html(parameters.l10n.label); popup.show(desc, imgs, cardStyles ? cardStyles.back : undefined, function (refocus) { if (isFinished) { // Game done finished(); card.makeUntabbable(); } else { // Popup is closed, continue. timer.play(); if (refocus) { card.setFocus(); } } }); } else if (isFinished) { // Game done finished(); card.makeUntabbable(); } }; /** * Game has finished! * @param {object} [params] Parameters. * @param {boolean} [params.restoring] True if restoring state. * @private */ var finished = function (params = {}) { if (!params.restoring) { timer.stop(); } score = 1; if (parameters.behaviour && parameters.behaviour.allowRetry) { // Create retry button self.retryButton = createButton('reset', parameters.l10n.tryAgain || 'Reset', () => { removeRetryButton(); self.resetTask(true); }); self.retryButton.style.fontSize = (parseFloat($wrapper.children('ul')[0].style.fontSize) * 0.75) + 'px'; const retryModal = document.createElement('div'); retryModal.setAttribute('role', 'dialog'); retryModal.setAttribute('aria-modal', 'true'); retryModal.setAttribute('aria-describedby', 'modalDescription'); retryModal.setAttribute('tabindex', -1); const status = document.createElement('div'); status.style.width = '1px'; status.style.height = '1px'; status.setAttribute('id', 'modalDescription'); status.innerText = `${$feedback[0].innerHTML} ${parameters.l10n.done} ${$status[0].innerText}`.replace(/\n/g, " "); retryModal.appendChild(status); retryModal.appendChild(self.retryButton); $bottom[0].appendChild(retryModal); // Add to DOM retryModal.focus(); } $feedback.addClass('h5p-show'); // Announce if (!params.restoring) { self.trigger(self.createXAPICompletedEvent()); } }; /** * Remove retry button. * @private */ const removeRetryButton = function () { if (!self.retryButton || self.retryButton.parentNode.parentNode !== $bottom[0]) { return; // Button not defined or attached to wrapper } self.retryButton.classList.add('h5p-memory-transout'); }; /** * Shuffle the cards and restart the game! * @private */ var resetGame = function (moveFocus = false) { // Reset cards score = 0; flipped = undefined; // Remove feedback $feedback[0].classList.remove('h5p-show'); popup.close(); // Reset timer and counter timer.stop(); timer.reset(); counter.reset(); flipBackCards(); // Randomize cards H5P.shuffleArray(cards); setTimeout(() => { // Re-append to DOM after flipping back for (var i = 0; i < cards.length; i++) { cards[i].reAppend(); } for (var j = 0; j < cards.length; j++) { cards[j].reset(); } // Scale new layout $wrapper.children('ul').children('.h5p-row-break').removeClass('h5p-row-break'); maxWidth = -1; self.trigger('resize'); moveFocus && cards[0].setFocus(); if (self.retryButton) { $bottom[0].removeChild(self.retryButton.parentNode); } }, 600); }; /** * Game has finished! * @private */ var createButton = function (name, label, action) { var buttonElement = document.createElement('button'); buttonElement.classList.add('h5p-memory-' + name); buttonElement.innerHTML = label; buttonElement.addEventListener('click', action, false); return buttonElement; }; /** * Flip back all cards unless pair found or excluded. * @param {object} [params] Parameters. * @param {H5P.MemoryGame.Card[]} [params.excluded] Cards to exclude from flip back. * @param {boolean} [params.keepPairs] True to keep pairs that were found. */ var flipBackCards = (params = {}) => { cards.forEach((card) => { params.excluded = params.excluded ?? []; params.keepPairs = params.keepPairs ?? false; if (params.excluded.includes(card)) { return; // Skip the card that was flipped } if (params.keepPairs) { const mate = getCardMate(card); if ( mate.isFlipped() && card.isFlipped() && !params.excluded.includes(mate) ) { return; } } card.flipBack(); }); }; /** * Get mate of a card. * @param {H5P.MemoryGame.Card} card Card. * @returns {H5P.MemoryGame.Card} Mate of the card. * @private */ var getCardMate = (card) => { const idSegments = card.getId().split('-'); return cards.find((mate) => { const mateIdSegments = mate.getId().split('-'); return ( idSegments[0] === mateIdSegments[0] && idSegments[1] !== mateIdSegments[1] ); }); } /** * Adds card to card list and set up a flip listener. * * @private * @param {H5P.MemoryGame.Card} card * @param {H5P.MemoryGame.Card} mate */ var addCard = function (card, mate) { card.on('flip', (event) => { self.answerGiven = true; if (getNumFlipped() === 3 && !event.data?.restoring) { // Flip back all cards except the one that was just flipped flipBackCards({ excluded: [card], keepPairs: true }); } if (audioCard) { audioCard.stopAudio(); } if (!event.data?.restoring) { popup.close(); self.triggerXAPI('interacted'); // Keep track of time spent timer.play(); } // Announce the card unless it's the last one and it's correct var isMatched = (flipped === mate); var isLast = cards.every((card) => card.isRemoved()); card.updateLabel(isMatched, !(isMatched && isLast)); let okToCheck = false; if (flipped !== undefined) { var matie = flipped; // Reset the flipped card. flipped = undefined; if (!event.data?.restoring) { okToCheck = true; } } else { flipped = card; } if (!event.data?.restoring) { // Always return focus to the card last flipped for (var i = 0; i < cards.length; i++) { cards[i].makeUntabbable(); } (flipped || card).makeTabbable(); // Count number of cards turned counter.increment(); } if (okToCheck) { check(card, matie, mate); } }); card.on('audioplay', function () { if (audioCard) { audioCard.stopAudio(); } audioCard = card; }); card.on('audiostop', function () { audioCard = undefined; }); /** * Create event handler for moving focus to next available card i * given direction. * * @private * @param {number} direction Direction code, see MemoryGame.DIRECTION_x. * @return {function} Focus handler. */ var createCardChangeFocusHandler = function (direction) { return function () { // Get current card index const currentIndex = cards.map(function (card) { return card.isTabbable; }).indexOf(true); if (currentIndex === -1) { return; // No tabbable card found } // Skip cards that have already been removed from the game let adjacentIndex = currentIndex; do { adjacentIndex = getAdjacentCardIndex(adjacentIndex, direction); } while (adjacentIndex !== null && cards[adjacentIndex].isRemoved()); if (adjacentIndex === null) { return; // No card available in that direction } // Move focus cards[currentIndex].makeUntabbable(); cards[adjacentIndex].setFocus(); }; }; // Register handlers for moving focus in given direction card.on('up', createCardChangeFocusHandler(MemoryGame.DIRECTION_UP)); card.on('next', createCardChangeFocusHandler(MemoryGame.DIRECTION_RIGHT)); card.on('down', createCardChangeFocusHandler(MemoryGame.DIRECTION_DOWN)); card.on('prev', createCardChangeFocusHandler(MemoryGame.DIRECTION_LEFT)); /** * Create event handler for moving focus to the first or the last card * on the table. * * @private * @param {number} direction +1/-1 * @return {function} */ var createEndCardFocusHandler = function (direction) { return function () { var focusSet = false; for (var i = 0; i < cards.length; i++) { var j = (direction === -1 ? cards.length - (i + 1) : i); if (!focusSet && !cards[j].isRemoved()) { cards[j].setFocus(); focusSet = true; } else if (cards[j] === card) { card.makeUntabbable(); } } }; }; // Register handlers for moving focus to first and last card card.on('first', createEndCardFocusHandler(1)); card.on('last', createEndCardFocusHandler(-1)); cards.push(card); }; var cardStyles, invertShades; if (parameters.lookNFeel) { // If the contrast between the chosen color and white is too low we invert the shades to create good contrast invertShades = (parameters.lookNFeel.themeColor && getContrast(parameters.lookNFeel.themeColor) < 1.7 ? -1 : 1); var backImage = (parameters.lookNFeel.cardBack ? H5P.getPath(parameters.lookNFeel.cardBack.path, id) : null); cardStyles = MemoryGame.Card.determineStyles(parameters.lookNFeel.themeColor, invertShades, backImage); } // Determine number of cards to be used const numCardsToUse = Math.max( 2, parseInt(parameters.behaviour?.numCardsToUse ?? parameters.cards.length) ); // Create cards pool let cardsPool = parameters.cards .reduce((result, cardParams, index) => { // Create first card const cardOne = new MemoryGame.Card(cardParams.image, id, 2 * numCardsToUse, cardParams.imageAlt, parameters.l10n, cardParams.description, cardStyles, cardParams.audio, `${index}-1`); let cardTwo; if (MemoryGame.Card.hasTwoImages(cardParams)) { // Use matching image for card two cardTwo = new MemoryGame.Card(cardParams.match, id, 2 * numCardsToUse, cardParams.matchAlt, parameters.l10n, cardParams.description, cardStyles, cardParams.matchAudio, `${index}-2`); cardOne.hasTwoImages = cardTwo.hasTwoImages = true; } else { // Add two cards with the same image cardTwo = new MemoryGame.Card(cardParams.image, id, 2 * numCardsToUse, cardParams.imageAlt, parameters.l10n, cardParams.description, cardStyles, cardParams.audio, `${index}-2`); } return [...result, cardOne, cardTwo]; }, []); let cardOrder; if (this.previousState.cards) { cardOrder = this.previousState.cards.map((cardState) => cardState.id); } else { while (cardsPool.length > 2 * numCardsToUse) { // Extract unique indexex from the current cardsPool const uniqueCardIndexes = Array.from(new Set(cardsPool.map(card => card.getId().split('-')[0]))); // Remove cards with randomly selected index const indexToRemove = uniqueCardIndexes[Math.floor(Math.random() * uniqueCardIndexes.length)]; cardsPool = cardsPool.filter(card => card.getId().split('-')[0] !== indexToRemove); } cardOrder = cardsPool.map((card) => card.getId()); H5P.shuffleArray(cardOrder); } // Create cards to be used in the game cardOrder.forEach((cardId) => { const card = cardsPool.find((card) => card.getId() === cardId); const matchId = (cardId.split('-')[1] === '1') ? cardId.replace('-1', '-2') : cardId.replace('-2', '-1') const match = cardsPool.find((card) => card.getId() === matchId); addCard(card, match); }); // Restore state of cards this.previousState.cards?.forEach((cardState) => { const card = cards.find((card) => card.getId() === cardState.id); if (!card) { return; } if (cardState.flipped) { card.flip({ restoring: true }); } if (cardState.removed) { card.remove(); } /* * Keep track of the flipped card. When restoring 1/3 flipped cards, * we need to ensure that the non-matching card is set as flipped */ if (getNumFlipped() % 2 === 1) { flipped = cards .filter((card) => { return card.isFlipped() && !getCardMate(card).isFlipped(); }) .shift(); } }); // Ensure all cards are removed if state was stored during flip time period if (cards.every((card) => card.isFlipped())) { cards.forEach((card) => card.remove()); } // Set score before DOM is attached to page if (cards.every((card) => card.isRemoved())) { score = 1; } // Build DOM elements to be attached later var $list = $('<ul/>', { role: 'application', 'aria-labelledby': 'h5p-intro-' + numInstances }); for (var i = 0; i < cards.length; i++) { cards[i].appendTo($list); } if (cards.length) { // Make first available card tabbable cards.filter((card) => !card.isRemoved())[0]?.makeTabbable(); $applicationLabel = $('<div/>', { id: 'h5p-intro-' + numInstances, 'class': 'h5p-memory-hidden-read', html: parameters.l10n.label + ' ' + parameters.l10n.labelInstructions, }); $bottom = $('<div/>', { 'class': 'h5p-programatically-focusable' }); $feedback = $('<div class="h5p-feedback">' + parameters.l10n.feedback + '</div>').appendTo($bottom); // Add status bar var $status = $('<dl class="h5p-status">' + '<dt>' + parameters.l10n.timeSpent + ':</dt>' + '<dd class="h5p-time-spent"><time role="timer" datetime="PT0M0S">0:00</time><span class="h5p-memory-hidden-read">.</span></dd>' + '<dt>' + parameters.l10n.cardTurns + ':</dt>' + '<dd class="h5p-card-turns">0<span class="h5p-memory-hidden-read">.</span></dd>' + '</dl>').appendTo($bottom); timer = new MemoryGame.Timer( $status.find('time')[0], this.previousState.timer ?? 0 ); counter = new MemoryGame.Counter( $status.find('.h5p-card-turns'), this.previousState.counter ?? 0 ); popup = new MemoryGame.Popup(parameters.l10n); popup.on('closed', function () { // Add instructions back $applicationLabel.html(parameters.l10n.label + ' ' + parameters.l10n.labelInstructions); }); // Aria live region to politely read to screen reader ariaLiveRegion = new MemoryGame.AriaLiveRegion(); } else { const $foo = $('<div/>') .text('No card was added to the memory game!') .appendTo($list); $list.appendTo($wrapper); } /** * Attach this game's html to the given container. * * @param {H5P.jQuery} $container */ self.attach = function ($container) { this.triggerXAPI('attempted'); // TODO: Only create on first attach! $wrapper = $container.addClass('h5p-memory-game').html(''); if (invertShades === -1) { $container.addClass('h5p-invert-shades'); } if (cards.length) { $applicationLabel.appendTo($wrapper); $list.appendTo($wrapper); $bottom.appendTo($wrapper); popup.appendTo($wrapper); $wrapper.append(ariaLiveRegion.getDOM()); $wrapper.click(function (e) { if (!popup.getElement()?.contains(e.target)) { popup.close(); } }); } else { $list.appendTo($wrapper); } // resize to scale game size and check for finished game afterwards this.trigger('resize'); window.requestAnimationFrame(() => { if (cards.length && cards.every((card) => card.isRemoved())) { finished({ restoring: true }); } }); self.attached = true; /* * DOM is only created here in `attach`, so it cannot necessarily be reset * by `resetTask` if using MemoryGame as subcontent after resuming. */ if (this.shouldResetDOMOnAttach) { removeRetryButton(); resetGame(); this.shouldResetDOMOnAttach = false; } }; /** * Will try to scale the game so that it fits within its container. * Puts the cards into a grid layout to make it as square as possible – * which improves the playability on multiple devices. * * @private */ var scaleGameSize = function () { // Check how much space we have available var $list = $wrapper.children('ul'); var newMaxWidth = parseFloat(window.getComputedStyle($list[0]).width); if (maxWidth === newMaxWidth) { return; // Same size, no need to recalculate } else { maxWidth = newMaxWidth; } // Get the card holders var $elements = $list.children(); if ($elements.length < 4) { return; // No need to proceed } // Determine the optimal number of columns var newNumCols = Math.ceil(Math.sqrt($elements.length)); // Do not exceed the max number of columns var maxCols = Math.floor(maxWidth / CARD_MIN_SIZE); if (newNumCols > maxCols) { newNumCols = maxCols; } if (numCols !== newNumCols) { // We need to change layout numCols = newNumCols; // Calculate new column size in percentage and round it down (we don't // want things sticking out…) var colSize = Math.floor((100 / numCols) * 10000) / 10000; $elements.css('width', colSize + '%').each(function (i, e) { $(e).toggleClass('h5p-row-break', i === numCols); }); } // Calculate how much one percentage of the standard/default size is var onePercentage = ((CARD_STD_SIZE * numCols) + STD_FONT_SIZE) / 100; var paddingSize = (STD_FONT_SIZE * LIST_PADDING) / onePercentage; var cardSize = (100 - paddingSize) / numCols; var fontSize = (((maxWidth * (cardSize / 100)) * STD_FONT_SIZE) / CARD_STD_SIZE); // We use font size to evenly scale all parts of the cards. $list.css('font-size', fontSize + 'px'); popup.setSize(fontSize); // due to rounding errors in browsers the margins may vary a bit… }; /** * Get index of adjacent card. * * @private * @param {number} currentIndex Index of card to check adjacent card for. * @param {number} direction Direction code, cmp. MemoryGame.DIRECTION_x. * @returns {number|null} Index of adjacent card or null if not retrievable. */ const getAdjacentCardIndex = function (currentIndex, direction) { if ( typeof currentIndex !== 'number' || currentIndex < 0 || currentIndex > cards.length - 1 || ( direction !== MemoryGame.DIRECTION_UP && direction !== MemoryGame.DIRECTION_RIGHT && direction !== MemoryGame.DIRECTION_DOWN && direction !== MemoryGame.DIRECTION_LEFT ) ) { return null; // Parameters not valid } let adjacentIndex = null; if (direction === MemoryGame.DIRECTION_LEFT) { adjacentIndex = currentIndex - 1; } else if (direction === MemoryGame.DIRECTION_RIGHT) { adjacentIndex = currentIndex + 1; } else if (direction === MemoryGame.DIRECTION_UP) { adjacentIndex = currentIndex - numCols; } else if (direction === MemoryGame.DIRECTION_DOWN) { adjacentIndex = currentIndex + numCols; } return (adjacentIndex >= 0 && adjacentIndex < cards.length) ? adjacentIndex : null; // Out of bounds } if (parameters.behaviour && parameters.behaviour.useGrid && numCardsToUse) { self.on('resize', () => { scaleGameSize(); if (self.retryButton) { self.retryButton.style.fontSize = (parseFloat($wrapper.children('ul')[0].style.fontSize) * 0.75) + 'px'; } }); } /** * Determine whether the task was answered already. * @returns {boolean} True if answer was given by user, else false. * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-1} */ self.getAnswerGiven = () => { return self.answerGiven; } /** * Get the user's score for this task. * * @returns {Number} The current score. */ self.getScore = function () { return score; }; /** * Get the maximum score for this task. * * @returns {Number} The maximum score. */ self.getMaxScore = function () { return 1; }; /** * Create a 'completed' xAPI event object. * * @returns {Object} xAPI completed event */ self.createXAPICompletedEvent = function () { var completedEvent = self.createXAPIEventTemplate('completed'); completedEvent.setScoredResult(self.getScore(), self.getMaxScore(), self, true, true); completedEvent.data.statement.result.duration = 'PT' + (Math.round(timer.getTime() / 10) / 100) + 'S'; return completedEvent; } /** * Contract used by report rendering engine. * * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6} * * @returns {Object} xAPI data */ self.getXAPIData = function () { var completedEvent = self.createXAPICompletedEvent(); return { statement: completedEvent.data.statement }; }; /** * Reset task. * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-5} */ self.resetTask = function (moveFocus = false) { if (self.attached) { resetGame(moveFocus); } else { /* * DOM is only created in `attach`, so it cannot necessarily be reset * here if using MemoryGame as subcontent after resuming. Schedule for * when DOM is attached. */ this.shouldResetDOMOnAttach = true; } this.wasReset = true; this.answerGiven = false; this.previousState = {}; delete this.cardOrder; }; /** * Get current state. * @returns {object} Current state to be retrieved later. * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-7} */ self.getCurrentState = () => { if (!this.getAnswerGiven()) { return this.wasReset ? {} : undefined; } cardsState = cards.map((card) => { const flipped = card.isFlipped(); const removed = card.isRemoved(); return { id: card.getId(), // Just saving some bytes in user state database table ...(flipped && { flipped: flipped }), ...(removed && { removed: removed }) } }); return { timer: timer.getTime(), counter: counter.getCount(), cards: cardsState } } } // Extends the event dispatcher MemoryGame.prototype = Object.create(EventDispatcher.prototype); MemoryGame.prototype.constructor = MemoryGame; /** @constant {number} DIRECTION_UP Code for up. */ MemoryGame.DIRECTION_UP = 0; /** @constant {number} DIRECTION_LEFT Code for left. Legacy value. */ MemoryGame.DIRECTION_LEFT = -1; /** @constant {number} DIRECTION_DOWN Code for down. */ MemoryGame.DIRECTION_DOWN = 2; /** @constant {number} DIRECTION_DOWN Code for right. Legacy value. */ MemoryGame.DIRECTION_RIGHT = 1 /** * Determine color contrast level compared to white(#fff) * * @private * @param {string} color hex code * @return {number} From 1 to Infinity. */ var getContrast = function (color) { return 255 / ((parseInt(color.substring(1, 3), 16) * 299 + parseInt(color.substring(3, 5), 16) * 587 + parseInt(color.substring(5, 7), 16) * 144) / 1000); }; return MemoryGame; })(H5P.EventDispatcher, H5P.jQuery); ; (function (MemoryGame, EventDispatcher, $) { /** * @private * @constant {number} WCAG_MIN_CONTRAST_AA_LARGE Minimum contrast ratio. * @see https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html */ const WCAG_MIN_CONTRAST_AA_LARGE = 3; /** * Controls all the operations for each card. * * @class H5P.MemoryGame.Card * @extends H5P.EventDispatcher * @param {Object} image * @param {number} contentId * @param {number} cardsTotal Number of cards in total. * @param {string} alt * @param {Object} l10n Localization * @param {string} [description] * @param {Object} [styles] * @param {string} id Unique identifier for card including original+match info. */ MemoryGame.Card = function (image, contentId, cardsTotal, alt, l10n, description, styles, audio, id) { /** @alias H5P.MemoryGame.Card# */ var self = this; this.id = id; // Keep track of tabbable state self.isTabbable = false; // Initialize event inheritance EventDispatcher.call(self); let path, width, height, $card, $wrapper, $image, removedState, flippedState, audioPlayer; /** * Process HTML escaped string for use as attribute value, * e.g. for alt text or title attributes. * * @param {string} value * @return {string} WARNING! Do NOT use for innerHTML. */ const massageAttributeOutput = (value = 'Missing description') => { const dparser = new DOMParser().parseFromString(value, 'text/html'); const div = document.createElement('div'); div.innerHTML = dparser.documentElement.textContent;; return div.textContent || div.innerText; }; self.buildDOM = () => { $wrapper = $('<li class="h5p-memory-wrap" tabindex="-1" role="button"><div class="h5p-memory-card">' + '<div class="h5p-front"' + (styles && styles.front ? styles.front : '') + '>' + (styles && styles.backImage ? '' : '<span></span>') + '</div>' + '<div class="h5p-back"' + (styles && styles.back ? styles.back : '') + '>' + (path ? '<img src="' + path + '" alt="" style="width:' + width + ';height:' + height + '"/>' + (audioPlayer ? '<div class="h5p-memory-audio-button"></div>' : '') : '<i class="h5p-memory-audio-instead-of-image">') + '</div>' + '</div></li>'); $wrapper.on('keydown', (event) => { switch (event.code) { case 'Enter': case 'Space': self.flip(); event.preventDefault(); return; case 'ArrowRight': // Move focus forward self.trigger('next'); event.preventDefault(); return; case 'ArrowDown': // Move focus down self.trigger('down'); event.preventDefault(); return; case 'ArrowLeft': // Move focus back self.trigger('prev'); event.preventDefault(); return; case 'ArrowUp': // Up // Move focus up self.trigger('up'); event.preventDefault(); return; case 'End': // Move to last card self.trigger('last'); event.preventDefault(); return; case 'Home': // Move to first card self.trigger('first'); event.preventDefault(); return; } }); $image = $wrapper.find('img'); $card = $wrapper.children('.h5p-memory-card') .children('.h5p-front') .click(function (e) { e.stopPropagation(); self.flip(); }) .end(); if (audioPlayer) { $card.children('.h5p-back') .click(function () { if ($card.hasClass('h5p-memory-audio-playing')) { self.stopAudio(); } else { audioPlayer.play(); } }) } } // alt = alt || 'Missing description'; // Default for old games alt = massageAttributeOutput(alt); if (image && image.path) { path = H5P.getPath(image.path, contentId); if (image.width !== undefined && image.height !== undefined) { if (image.width > image.height) { width = '100%'; height = 'auto'; } else { height = '100%'; width = 'auto'; } } else { width = height = '100%'; } } if (audio) { // Check if browser supports audio. audioPlayer = document.createElement('audio'); if (audioPlayer.canPlayType !== undefined) { // Add supported source files. for (var i = 0; i < audio.length; i++) { if (audioPlayer.canPlayType(audio[i].mime)) { var source = document.createElement('source'); source.src = H5P.getPath(audio[i].path, contentId); source.type = audio[i].mime; audioPlayer.appendChild(source); } } } if (!audioPlayer.children.length) { audioPlayer = null; // Not supported } else { audioPlayer.controls = false; audioPlayer.preload = 'auto'; var handlePlaying = function () { if ($card) { $card.addClass('h5p-memory-audio-playing'); self.trigger('audioplay'); } }; var handleStopping = function () { if ($card) { $card.removeClass('h5p-memory-audio-playing'); self.trigger('audiostop'); } }; audioPlayer.addEventListener('play', handlePlaying); audioPlayer.addEventListener('ended', handleStopping); audioPlayer.addEventListener('pause', handleStopping); } } this.buildDOM(); /** * Get id of the card. * @returns {string} The id of the card. (originalIndex-sideNumber) */ this.getId = () => { return self.id; }; /** * Update the cards label to make it accessible to users with a readspeaker * * @param {boolean} isMatched The card has been matched * @param {boolean} announce Announce the current state of the card * @param {boolean} reset Go back to the default label */ self.updateLabel = function (isMatched, announce, reset) { // Determine new label from input params const imageAlt = alt ? ` ${alt}`: ''; let label = reset ? l10n.cardUnturned : `${l10n.cardTurned}${imageAlt}`; if (isMatched) { label = l10n.cardMatched + ' ' + label; } // Update the card's label $wrapper.attr('aria-label', l10n.cardPrefix .replace('%num', $wrapper.index() + 1) .replace('%total', cardsTotal) + ' ' + label); // Update disabled property $wrapper.attr('aria-disabled', reset ? null : 'true'); // Announce the label change if (announce) { $wrapper.blur().focus(); // Announce card label } }; /** * Flip card. * * Win 11 screen reader announces image's alt tag even though it never gets * focus and button provides aria-label. Therefore alt tag is only set when * card is turned. * @param {object} [params] Parameters. * @param {boolean} [params.restoring] True if card is being restored from a saved state. */ self.flip = function (params = {}) { if (flippedState) { $wrapper.blur().focus(); // Announce card label again return; } $card.addClass('h5p-flipped'); $image.attr('alt', alt); flippedState = true; if (audioPlayer && !params.restoring) { audioPlayer.play(); } this.trigger('flip', { restoring: params.restoring }); }; /** * Flip card back. */ self.flipBack = function () { self.stopAudio(); self.updateLabel(null, null, true); // Reset card label $card.removeClass('h5p-flipped'); $image.attr('alt', ''); flippedState = false; }; /** * Remove. */ self.remove = function () { $card.addClass('h5p-matched'); removedState = true; }; /** * Reset card to natural state */ self.reset = function () { self.stopAudio(); self.updateLabel(null, null, true); // Reset card label flippedState = false; removedState = false; $card[0].classList.remove('h5p-flipped', 'h5p-matched'); }; /** * Get card description. * * @returns {string} */ self.getDescription = function () { return description; }; /** * Get image clone. * * @returns {H5P.jQuery} */ self.getImage = function () { return $card.find('img').clone(); }; /** * Append card to the given container. * * @param {H5P.jQuery} $container */ self.appendTo = function ($container) { $wrapper.appendTo($container); $wrapper.attr( 'aria-label', l10n.cardPrefix .replace('%num', $wrapper.index() + 1) .replace('%total', cardsTotal) + ' ' + l10n.cardUnturned ); }; /** * Re-append to parent container. */ self.reAppend = function () { var parent = $wrapper[0].parentElement; parent.appendChild($wrapper[0]); }; /** * Make the card accessible when tabbing */ self.makeTabbable = function () { if ($wrapper) { $wrapper.attr('tabindex', '0'); this.isTabbable = true; } }; /** * Prevent tabbing to the card */ self.makeUntabbable = function () { if ($wrapper) { $wrapper.attr('tabindex', '-1'); this.isTabbable = false; } }; /** * Make card tabbable and move focus to it */ self.setFocus = function () { self.makeTabbable(); if ($wrapper) { $wrapper.focus(); } }; /** * Check if the card has been removed from the game, i.e. if has * been matched. */ this.isRemoved = () => { return removedState ?? false; }; /** * Determine whether card is flipped or not. * @returns {boolean} True if card is flipped, else false. */ this.isFlipped = () => { return flippedState ?? false; } /** * Stop any audio track that might be playing. */ self.stopAudio = function () { if (audioPlayer) { audioPlayer.pause(); audioPlayer.currentTime = 0; } }; }; // Extends the event dispatcher MemoryGame.Card.prototype = Object.create(EventDispatcher.prototype); MemoryGame.Card.prototype.constructor = MemoryGame.Card; /** * Check to see if the given object corresponds with the semantics for * a memory game card. * * @param {object} params * @returns {boolean} */ MemoryGame.Card.isValid = function (params) { return (params !== undefined && (params.image !== undefined && params.image.path !== undefined) || params.audio); }; /** * Checks to see if the card parameters should create cards with different * images. * * @param {object} params * @returns {boolean} */ MemoryGame.Card.hasTwoImages = function (params) { return (params !== undefined && (params.match !== undefined && params.match.path !== undefined) || params.matchAudio); }; /** * Determines the theme for how the cards should look * * @param {string} color The base color selected * @param {number} invertShades Factor used to invert shades in case of bad contrast */ MemoryGame.Card.determineStyles = function (color, invertShades, backImage) { var styles = { front: '', back: '', backImage: !!backImage }; // Create color theme if (color) { const frontColor = shadeEnforceContrast(color, 43.75 * invertShades); const backColor = shade(frontColor, 12.75 * invertShades); styles.front += 'color:' + color + ';' + 'background-color:' + frontColor + ';' + 'border-color:' + frontColor +';'; styles.back += 'color:' + color + ';' + 'background-color:' + backColor + ';' + 'border-color:' + frontColor +';'; } // Add back image for card if (backImage) { var backgroundImage = "background-image:url('" + backImage + "')"; styles.front += backgroundImage; styles.back += backgroundImage; } // Prep style attribute if (styles.front) { styles.front = ' style="' + styles.front + '"'; } if (styles.back) { styles.back = ' style="' + styles.back + '"'; } return styles; }; /** * Get RGB color components from color hex value. * * @private * @param {string} color Color as hex value, e.g. '#123456`. * @returns {number[]} Red, green, blue color component as integer from 0-255. */ const getRGB = function (color) { return [ parseInt(color.substring(1, 3), 16), parseInt(color.substring(3, 5), 16), parseInt(color.substring(5, 7), 16) ]; } /** * Compute luminance for color. * * @private * @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef * @param {string} color Color as hex value, e.g. '#123456`. * @returns {number} Luminance, [0-1], 0 = lightest, 1 = darkest. */ const computeLuminance = function (color) { const rgba = getRGB(color) .map(function (v) { v = v / 255; return v < 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return rgba[0] * 0.2126 + rgba[1] * 0.7152 + rgba[2] * 0.0722; } /** * Compute relative contrast between two colors. * * @private * @see https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html * @param {string} color1 Color as hex value, e.g. '#123456`. * @param {string} color2 Color as hex value, e.g. '#123456`. * @returns {number} Contrast, [1-21], 1 = no contrast, 21 = max contrast. */ const computeContrast = function (color1, color2) { const luminance1 = computeLuminance(color1); const luminance2 = computeLuminance(color2); return ( (Math.max(luminance1, luminance2) + 0.05) / (Math.min(luminance1, luminance2) + 0.05) ) } /** * Use shade function, but enforce minimum contrast * * @param {string} color Color as hex value, e.g. '#123456`. * @param {number} percent Shading percentage. * @returns {string} Color as hex value, e.g. '#123456`. */ const shadeEnforceContrast = function (color, percent) { let shadedColor; do { shadedColor = shade(color, percent); if (shadedColor === '#ffffff' || shadedColor === '#000000') { // Cannot brighten/darken, make original color 5% points darker/brighter color = shade(color, -5 * Math.sign(percent)); } else { // Increase shading by 5 percent percent = percent * 1.05; } } while (computeContrast(color, shadedColor) < WCAG_MIN_CONTRAST_AA_LARGE); return shadedColor; } /** * Convert hex color into shade depending on given percent * * @private * @param {string} color * @param {number} percent * @return {string} new color */ var shade = function (color, percent) { var newColor = '#'; // Determine if we should lighten or darken var max = (percent < 0 ? 0 : 255); // Always stay positive if (percent < 0) { percent *= -1; } percent /= 100; for (var i = 1; i < 6; i += 2) { // Grab channel and convert from hex to dec var channel = parseInt(color.substring(i, i + 2), 16); // Calculate new shade and convert back to hex channel = (Math.round((max - channel) * percent) + channel).toString(16); // Make sure to always use two digits newColor += (channel.length < 2 ? '0' + channel : channel); } return newColor; }; })(H5P.MemoryGame, H5P.EventDispatcher, H5P.jQuery); ; (function (MemoryGame) { /** * Keeps track of the number of cards that has been turned * * @class H5P.MemoryGame.Counter * @param {H5P.jQuery} $container */ MemoryGame.Counter = function ($container, startValue = 0) { /** @alias H5P.MemoryGame.Counter# */ var self = this; var current = startValue; /** * @private */ self.update = function () { $container[0].innerText = current; }; /** * Get current count. * @returns {number} Current count. */ self.getCount = () => { return current; } /** * Increment the counter. */ self.increment = function () { current++; self.update(); }; /** * Revert counter back to its natural state */ self.reset = function () { current = 0; self.update(); }; self.update(); }; })(H5P.MemoryGame); ; (function (MemoryGame, EventDispatcher, $) { /** * A dialog for reading the description of a card. * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/ * * @class H5P.MemoryGame.Popup * @extends H5P.EventDispatcher * @param {Object.<string, string>} l10n */ MemoryGame.Popup = function (l10n) { // Initialize event inheritance EventDispatcher.call(this); /** @alias H5P.MemoryGame.Popup# */ var self = this; var closed; const $popupContainer = $( '<div class="h5p-memory-obscure-content"><div class="h5p-memory-pop" role="dialog" aria-modal="true"><div class="h5p-memory-top"></div><div class="h5p-memory-desc h5p-programatically-focusable" tabindex="-1"></div><div class="h5p-memory-close" role="button" tabindex="0" title="' + (l10n.closeLabel || 'Close') + '" aria-label="' + (l10n.closeLabel || 'Close') + '"></div></div></div>' ) .on('keydown', function (event) { if (event.code === 'Escape') { self.close(true); event.preventDefault(); } }) .hide(); const $popup = $popupContainer.find('.h5p-memory-pop'); const $top = $popupContainer.find('.h5p-memory-top'); // Hook up the close button const $closeButton = $popupContainer .find('.h5p-memory-close') .on('click', function () { self.close(true); }) .on('keydown', function (event) { if (event.code === 'Enter' || event.code === 'Space') { self.close(true); event.preventDefault(); } else if (event.code === 'Tab') { event.preventDefault(); // Lock focus } }); const $desc = $popupContainer .find('.h5p-memory-desc') .on('keydown', function (event) { if (event.code === 'Tab') { // Keep focus inside dialog $closeButton.focus(); event.preventDefault(); } }); /** * Append the popup to a container. * @param {H5P.jQuery} $container Container to append to. */ this.appendTo = ($container) => { $container.append($popupContainer); }; /** * Show the popup. * * @param {string} desc * @param {H5P.jQuery[]} imgs * @param {function} done */ self.show = function (desc, imgs, styles, done) { const announcement = '<span class="h5p-memory-screen-reader">' + l10n.cardMatchedA11y + '</span>' + desc; $desc.html(announcement); $top.html('').toggleClass('h5p-memory-two-images', imgs.length > 1); for (var i = 0; i < imgs.length; i++) { $('<div class="h5p-memory-image"' + (styles ? styles : '') + '></div>').append(imgs[i]).appendTo($top); } $popupContainer.show(); $desc.focus(); closed = done; }; /** * Close the popup. * * @param {boolean} refocus Sets focus after closing the dialog */ self.close = function (refocus) { if (closed !== undefined) { $popupContainer.hide(); closed(refocus); closed = undefined; self.trigger('closed'); } }; /** * Sets popup size relative to the card size * * @param {number} fontSize */ self.setSize = function (fontSize) { // Set image size $top[0].style.fontSize = fontSize + 'px'; // Determine card size var cardSize = fontSize * 6.25; // From CSS // Set popup size $popupContainer[0].style.minWidth = (cardSize * 2.5) + 'px'; $popupContainer[0].style.minHeight = cardSize + 'px'; }; this.getElement = () => { return $popup[0]; } }; })(H5P.MemoryGame, H5P.EventDispatcher, H5P.jQuery); ; (function (MemoryGame, Timer) { /** * Adapter between memory game and H5P.Timer * * @class H5P.MemoryGame.Timer * @extends H5P.Timer * @param {Element} element */ MemoryGame.Timer = function (element, startValue = 0) { /** @alias H5P.MemoryGame.Timer# */ var self = this; // Initialize event inheritance Timer.call(self, 100); this.setClockTime(startValue); /** @private {string} */ var naturalState = element.innerText; /** * Set up callback for time updates. * Formats time stamp for humans. * * @private */ var update = function () { var time = self.getTime(); var hours = Timer.extractTimeElement(time, 'hours'); var minutes = Timer.extractTimeElement(time, 'minutes'); var seconds = Timer.extractTimeElement(time, 'seconds') % 60; // Update duration attribute element.setAttribute('datetime', 'PT' + hours + 'H' + minutes + 'M' + seconds + 'S'); // Add leading zero if (hours < 10) { hours = '0' + hours; } if (minutes < 10) { minutes = '0' + minutes; } if (seconds < 10) { seconds = '0' + seconds; } element.innerText = hours + ':' + minutes + ':' + seconds; }; // Setup default behavior self.notify('every_tenth_second', update); self.on('reset', function () { element.innerText = naturalState; self.notify('every_tenth_second', update); }); update(); }; // Inheritance MemoryGame.Timer.prototype = Object.create(Timer.prototype); MemoryGame.Timer.prototype.constructor = MemoryGame.Timer; })(H5P.MemoryGame, H5P.Timer); ; (function (MemoryGame) { /** * Aria live region for reading to screen reader. * * @class H5P.MemoryGame.Popup */ MemoryGame.AriaLiveRegion = function () { let readText, timeout = null; // Build dom with defaults const dom = document.createElement('div'); dom.classList.add('h5p-memory-aria-live-region'); dom.setAttribute('aria-live', 'polite'); dom.style.height = '1px'; dom.style.overflow = 'hidden'; dom.style.position = 'absolute'; dom.style.textIndent = '1px'; dom.style.top = '-1px'; dom.style.width = '1px'; /** * Get DOM of aria live region. * * @returns {HTMLElement} DOM of aria live region. */ this.getDOM = function () { return dom; } /** * Set class if default CSS values do not suffice. * * @param {string} className Class name to set. Add CSS elsewhere. */ this.setClass = function(className) { if (typeof className !== 'string') { return; } // Remove default values dom.style.height = ''; dom.style.overflow = ''; dom.style.position = ''; dom.style.textIndent = ''; dom.style.top = ''; dom.style.width = ''; dom.classList = className; } /** * Read text via aria live region. * * @param {string} text Text to read. */ this.read = function (text) { if (readText) { const lastChar = readText .substring(readText.length - 1); readText = [`${readText}${lastChar === '.' ? '' : '.'}`, text] .join(' '); } else { readText = text; } dom.innerText = readText; window.clearTimeout(timeout); timeout = window.setTimeout(function () { readText = null; dom.innerText = ''; }, 100); } } })(H5P.MemoryGame); ; H5P.AdvancedText = (function ($, EventDispatcher) { /** * A simple library for displaying text with advanced styling. * * @class H5P.AdvancedText * @param {Object} parameters * @param {Object} [parameters.text='New text'] * @param {number} id */ function AdvancedText(parameters, id) { var self = this; EventDispatcher.call(this); var html = (parameters.text === undefined ? '<em>New text</em>' : parameters.text); /** * Wipe container and add text html. * * @alias H5P.AdvancedText#attach * @param {H5P.jQuery} $container */ self.attach = function ($container) { $container.addClass('h5p-advanced-text').html(html); }; } AdvancedText.prototype = Object.create(EventDispatcher.prototype); AdvancedText.prototype.constructor = AdvancedText; return AdvancedText; })(H5P.jQuery, H5P.EventDispatcher); ; /** * Accordion module * * @param {jQuery} $ */ H5P.Accordion = (function ($) { var nextIdPrefix = 0; var nextLooperId = 0; var allowedLoopers = []; /** * Initialize a new Accordion * * @class H5P.InteractiveVideo * @extends H5P.EventDispatcher * @param {Object} params Behavior settings * @param {Number} contentId Content identification * @param {Object} contentData Object containing task specific content data */ function Accordion(params, contentId, contentData) { this.contentId = contentId; H5P.EventDispatcher.call(this); // Set default behavior. this.params = $.extend({}, { hTag: "h2", panels: [] }, params); this.contentData = contentData; this.instances = []; for (var i = 0; i < this.params.panels.length; i++) { this.instances[i] = H5P.newRunnable(this.params.panels[i].content, contentId); } this.idPrefix = (nextIdPrefix++) + '-'; } Accordion.prototype = Object.create(H5P.EventDispatcher.prototype); Accordion.prototype.constructor = Accordion; /** * Append field to wrapper. * @param {jQuery} container the jQuery object which this module will attach itself to. */ Accordion.prototype.attach = function ($container) { var self = this; if (self.$content === undefined) { // Mark as consumed self.triggerConsumed(); // Create the content self.elements = []; for (var i = 0; i < self.params.panels.length; i++) { self.createPanel(i); } self.$content = $(self.elements); } // Insert content $container.html('').addClass('h5p-accordion').append(self.$content); }; /** * Create HTML for Panel. * @param {number} id */ Accordion.prototype.createPanel = function (id) { var self = this; var titleId = 'h5p-panel-link-' + this.idPrefix + id; var contentId = 'h5p-panel-content-' + self.idPrefix + id; var toggleCollapse = function () { if (self.$expandedTitle === undefined || !self.$expandedTitle.is($title)) { self.collapseExpandedPanels(); self.expandPanel($title, $titleButton, $content); } else { self.collapsePanel($title, $titleButton, $content); } // We're running in an iframe, so we must animate the iframe height self.animateResize(); }; // Create panel title var $title = $('<' + this.params.hTag + '/>', { 'id': titleId, 'class': 'h5p-panel-title', }); // Create panel button var $titleButton = $('<button/>', { 'class': 'h5p-panel-button', 'tabindex': '0', 'aria-expanded': 'false', 'aria-controls': contentId, 'html': self.params.panels[id].title, 'on': { 'click': toggleCollapse, 'keydown': function (event) { switch (event.keyCode) { case 38: // Up case 37: { // Left // Try to select previous item var $prev = $title.prev().prev().children('.h5p-panel-button'); if ($prev.length) { $prev.focus(); } return false; } case 40: // Down case 39: { // Right // Try to select next item var $next = $content.next().children('.h5p-panel-button'); if ($next.length) { $next.focus(); } return false; } case 32: // SPACE case 13: { // ENTER toggleCollapse(); return false; } } }, // The class needs to be set programmatically as the title // is not able to detect focus-visible on the button 'focus': function () { if($titleButton.is(':focus-visible')) { $title.addClass('h5p-panel-focused'); } }, 'blur': function () { $title.removeClass('h5p-panel-focused'); } } }); $title.append($titleButton); // Create panel content var $content = $('<div>', { 'id': contentId, 'class': 'h5p-panel-content', 'role': 'region', 'aria-labelledby': titleId, 'aria-hidden': 'true' }); // Add the content itself to the content section self.instances[id].attach($content); // Gather all content self.elements.push($title[0]); self.elements.push($content[0]); }; /** * Trigger the 'consumed' xAPI event when this commences * * (Will be more sophisticated in future version) */ Accordion.prototype.triggerConsumed = function () { var xAPIEvent = this.createXAPIEventTemplate({ id: 'http://activitystrea.ms/schema/1.0/consume', display: { 'en-US': 'consumed' } }, { result: { completion: true } }); this.trigger(xAPIEvent); }; /** * Collapse all expanded panels */ Accordion.prototype.collapseExpandedPanels = function () { var self = this; if (this.$expandedTitle !== undefined) { this.$expandedButton.attr('aria-expanded', false); this.$expandedTitle.removeClass('h5p-panel-expanded'); } if (this.$expandedPanel !== undefined) { this.$expandedPanel .stop(false, true) .slideUp(200, function () { self.stopWorkLoop(self.resizing); self.trigger('resize'); }) .attr('aria-hidden', true); } }; /** * Expand a panel * * @param {jQuery} $title The title of the panel that is to be expanded * @param {jQuery} $panel The panel that is to be expanded */ Accordion.prototype.expandPanel = function($title, $titleButton, $panel) { var self = this; $titleButton.attr('aria-expanded', true); $title.addClass('h5p-panel-expanded'); $panel .stop(false, true) .slideDown(200, function () { self.stopWorkLoop(self.resizing); self.trigger('resize'); }) .attr('aria-hidden', false); self.$expandedButton = $titleButton; self.$expandedTitle = $title; self.$expandedPanel = $panel; }; /** * Collapse a panel * * @param {jQuery} $title The title of the panel that is to be collapsed * @param {jQuery} $panel The panel that is to be collapsed */ Accordion.prototype.collapsePanel = function($title, $titleButton, $panel) { var self = this; $titleButton.attr('aria-expanded', false) $title.removeClass('h5p-panel-expanded'); $panel .stop(false, true) .slideUp(200, function () { self.stopWorkLoop(self.resizing); self.trigger('resize'); }) .attr('aria-hidden', true); self.$expandedTitle = self.$expandedButton = self.$expandedPanel = undefined; }; /** * Makes sure that the heigt of the iframe gets animated */ Accordion.prototype.animateResize = function () { var self = this; self.stopWorkLoop(this.resizing); this.resizing = self.startWorkLoop(function () { self.trigger('resize'); }, 40); }; Accordion.prototype.startWorkLoop = function (func, wait) { var myId = nextLooperId++; var self = this; allowedLoopers.push(myId); var looper = function(func, wait, myId) { return function () { if (self.allowedToWork(myId)) { try { func.call(null); } catch (e) { self.stopWorkLoop(myId); } setTimeout(looper, wait, func, wait, myId); } }; } (func, wait, myId); setTimeout(looper, wait); return myId; }; Accordion.prototype.stopWorkLoop = function (myId) { var index; while ((index = allowedLoopers.indexOf(myId)) !== -1) { allowedLoopers.splice(index, 1); } }; Accordion.prototype.allowedToWork = function (myId) { return allowedLoopers.indexOf(myId) !== -1; }; return Accordion; })(H5P.jQuery); ; var H5P = H5P || {}; /** * Transition contains helper function relevant for transitioning */ H5P.Transition = (function ($) { /** * @class * @namespace H5P */ Transition = {}; /** * @private */ Transition.transitionEndEventNames = { 'WebkitTransition': 'webkitTransitionEnd', 'transition': 'transitionend', 'MozTransition': 'transitionend', 'OTransition': 'oTransitionEnd', 'msTransition': 'MSTransitionEnd' }; /** * @private */ Transition.cache = []; /** * Get the vendor property name for an event * * @function H5P.Transition.getVendorPropertyName * @static * @private * @param {string} prop Generic property name * @return {string} Vendor specific property name */ Transition.getVendorPropertyName = function (prop) { if (Transition.cache[prop] !== undefined) { return Transition.cache[prop]; } var div = document.createElement('div'); // Handle unprefixed versions (FF16+, for example) if (prop in div.style) { Transition.cache[prop] = prop; } else { var prefixes = ['Moz', 'Webkit', 'O', 'ms']; var prop_ = prop.charAt(0).toUpperCase() + prop.substr(1); if (prop in div.style) { Transition.cache[prop] = prop; } else { for (var i = 0; i < prefixes.length; ++i) { var vendorProp = prefixes[i] + prop_; if (vendorProp in div.style) { Transition.cache[prop] = vendorProp; break; } } } } return Transition.cache[prop]; }; /** * Get the name of the transition end event * * @static * @private * @return {string} description */ Transition.getTransitionEndEventName = function () { return Transition.transitionEndEventNames[Transition.getVendorPropertyName('transition')] || undefined; }; /** * Helper function for listening on transition end events * * @function H5P.Transition.onTransitionEnd * @static * @param {domElement} $element The element which is transitioned * @param {function} callback The callback to be invoked when transition is finished * @param {number} timeout Timeout in milliseconds. Fallback if transition event is never fired */ Transition.onTransitionEnd = function ($element, callback, timeout) { // Fallback on 1 second if transition event is not supported/triggered timeout = timeout || 1000; Transition.transitionEndEventName = Transition.transitionEndEventName || Transition.getTransitionEndEventName(); var callbackCalled = false; var doCallback = function () { if (callbackCalled) { return; } $element.off(Transition.transitionEndEventName, callback); callbackCalled = true; clearTimeout(timer); callback(); }; var timer = setTimeout(function () { doCallback(); }, timeout); $element.on(Transition.transitionEndEventName, function () { doCallback(); }); }; /** * Wait for a transition - when finished, invokes next in line * * @private * * @param {Object[]} transitions Array of transitions * @param {H5P.jQuery} transitions[].$element Dom element transition is performed on * @param {number=} transitions[].timeout Timeout fallback if transition end never is triggered * @param {bool=} transitions[].break If true, sequence breaks after this transition * @param {number} index The index for current transition */ var runSequence = function (transitions, index) { if (index >= transitions.length) { return; } var transition = transitions[index]; H5P.Transition.onTransitionEnd(transition.$element, function () { if (transition.end) { transition.end(); } if (transition.break !== true) { runSequence(transitions, index+1); } }, transition.timeout || undefined); }; /** * Run a sequence of transitions * * @function H5P.Transition.sequence * @static * @param {Object[]} transitions Array of transitions * @param {H5P.jQuery} transitions[].$element Dom element transition is performed on * @param {number=} transitions[].timeout Timeout fallback if transition end never is triggered * @param {bool=} transitions[].break If true, sequence breaks after this transition */ Transition.sequence = function (transitions) { runSequence(transitions, 0); }; return Transition; })(H5P.jQuery); ; var H5P = H5P || {}; /** * Class responsible for creating a help text dialog */ H5P.JoubelHelpTextDialog = (function ($) { var numInstances = 0; /** * Display a pop-up containing a message. * * @param {H5P.jQuery} $container The container which message dialog will be appended to * @param {string} message The message * @param {string} closeButtonTitle The title for the close button * @return {H5P.jQuery} */ function JoubelHelpTextDialog(header, message, closeButtonTitle) { H5P.EventDispatcher.call(this); var self = this; numInstances++; var headerId = 'joubel-help-text-header-' + numInstances; var helpTextId = 'joubel-help-text-body-' + numInstances; var $helpTextDialogBox = $('<div>', { 'class': 'joubel-help-text-dialog-box', 'role': 'dialog', 'aria-labelledby': headerId, 'aria-describedby': helpTextId }); $('<div>', { 'class': 'joubel-help-text-dialog-background' }).appendTo($helpTextDialogBox); var $helpTextDialogContainer = $('<div>', { 'class': 'joubel-help-text-dialog-container' }).appendTo($helpTextDialogBox); $('<div>', { 'class': 'joubel-help-text-header', 'id': headerId, 'role': 'header', 'html': header }).appendTo($helpTextDialogContainer); $('<div>', { 'class': 'joubel-help-text-body', 'id': helpTextId, 'html': message, 'role': 'document', 'tabindex': 0 }).appendTo($helpTextDialogContainer); var handleClose = function () { $helpTextDialogBox.remove(); self.trigger('closed'); }; var $closeButton = $('<div>', { 'class': 'joubel-help-text-remove', 'role': 'button', 'title': closeButtonTitle, 'tabindex': 1, 'click': handleClose, 'keydown': function (event) { // 32 - space, 13 - enter if ([32, 13].indexOf(event.which) !== -1) { event.preventDefault(); handleClose(); } } }).appendTo($helpTextDialogContainer); /** * Get the DOM element * @return {HTMLElement} */ self.getElement = function () { return $helpTextDialogBox; }; self.focus = function () { $closeButton.focus(); }; } JoubelHelpTextDialog.prototype = Object.create(H5P.EventDispatcher.prototype); JoubelHelpTextDialog.prototype.constructor = JoubelHelpTextDialog; return JoubelHelpTextDialog; }(H5P.jQuery)); ; var H5P = H5P || {}; /** * Class responsible for creating auto-disappearing dialogs */ H5P.JoubelMessageDialog = (function ($) { /** * Display a pop-up containing a message. * * @param {H5P.jQuery} $container The container which message dialog will be appended to * @param {string} message The message * @return {H5P.jQuery} */ function JoubelMessageDialog ($container, message) { var timeout; var removeDialog = function () { $warning.remove(); clearTimeout(timeout); $container.off('click.messageDialog'); }; // Create warning popup: var $warning = $('<div/>', { 'class': 'joubel-message-dialog', text: message }).appendTo($container); // Remove after 3 seconds or if user clicks anywhere in $container: timeout = setTimeout(removeDialog, 3000); $container.on('click.messageDialog', removeDialog); return $warning; } return JoubelMessageDialog; })(H5P.jQuery); ; var H5P = H5P || {}; /** * Class responsible for creating a circular progress bar */ H5P.JoubelProgressCircle = (function ($) { /** * Constructor for the Progress Circle * * @param {Number} number The amount of progress to display * @param {string} progressColor Color for the progress meter * @param {string} backgroundColor Color behind the progress meter */ function ProgressCircle(number, progressColor, fillColor, backgroundColor) { progressColor = progressColor || '#1a73d9'; fillColor = fillColor || '#f0f0f0'; backgroundColor = backgroundColor || '#ffffff'; var progressColorRGB = this.hexToRgb(progressColor); //Verify number try { number = Number(number); if (number === '') { throw 'is empty'; } if (isNaN(number)) { throw 'is not a number'; } } catch (e) { number = 'err'; } //Draw circle if (number > 100) { number = 100; } // We can not use rgba, since they will stack on top of each other. // Instead we create the equivalent of the rgba color // and applies this to the activeborder and background color. var progressColorString = 'rgb(' + parseInt(progressColorRGB.r, 10) + ',' + parseInt(progressColorRGB.g, 10) + ',' + parseInt(progressColorRGB.b, 10) + ')'; // Circle wrapper var $wrapper = $('<div/>', { 'class': "joubel-progress-circle-wrapper" }); //Active border indicates progress var $activeBorder = $('<div/>', { 'class': "joubel-progress-circle-active-border" }).appendTo($wrapper); //Background circle var $backgroundCircle = $('<div/>', { 'class': "joubel-progress-circle-circle" }).appendTo($activeBorder); //Progress text/number $('<span/>', { 'text': number + '%', 'class': "joubel-progress-circle-percentage" }).appendTo($backgroundCircle); var deg = number * 3.6; if (deg <= 180) { $activeBorder.css('background-image', 'linear-gradient(' + (90 + deg) + 'deg, transparent 50%, ' + fillColor + ' 50%),' + 'linear-gradient(90deg, ' + fillColor + ' 50%, transparent 50%)') .css('border', '2px solid' + backgroundColor) .css('background-color', progressColorString); } else { $activeBorder.css('background-image', 'linear-gradient(' + (deg - 90) + 'deg, transparent 50%, ' + progressColorString + ' 50%),' + 'linear-gradient(90deg, ' + fillColor + ' 50%, transparent 50%)') .css('border', '2px solid' + backgroundColor) .css('background-color', progressColorString); } this.$activeBorder = $activeBorder; this.$backgroundCircle = $backgroundCircle; this.$wrapper = $wrapper; this.initResizeFunctionality(); return $wrapper; } /** * Initializes resize functionality for the progress circle */ ProgressCircle.prototype.initResizeFunctionality = function () { var self = this; $(window).resize(function () { // Queue resize setTimeout(function () { self.resize(); }); }); // First resize setTimeout(function () { self.resize(); }, 0); }; /** * Resize function makes progress circle grow or shrink relative to parent container */ ProgressCircle.prototype.resize = function () { var $parent = this.$wrapper.parent(); if ($parent !== undefined && $parent) { // Measurements var fontSize = parseInt($parent.css('font-size'), 10); // Static sizes var fontSizeMultiplum = 3.75; var progressCircleWidthPx = parseInt((fontSize / 4.5), 10) % 2 === 0 ? parseInt((fontSize / 4.5), 10) + 4 : parseInt((fontSize / 4.5), 10) + 5; var progressCircleOffset = progressCircleWidthPx / 2; var width = fontSize * fontSizeMultiplum; var height = fontSize * fontSizeMultiplum; this.$activeBorder.css({ 'width': width, 'height': height }); this.$backgroundCircle.css({ 'width': width - progressCircleWidthPx, 'height': height - progressCircleWidthPx, 'top': progressCircleOffset, 'left': progressCircleOffset }); } }; /** * Hex to RGB conversion * @param hex * @returns {{r: Number, g: Number, b: Number}} */ ProgressCircle.prototype.hexToRgb = function (hex) { var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; }; return ProgressCircle; }(H5P.jQuery)); ; var H5P = H5P || {}; H5P.SimpleRoundedButton = (function ($) { /** * Creates a new tip */ function SimpleRoundedButton(text) { var $simpleRoundedButton = $('<div>', { 'class': 'joubel-simple-rounded-button', 'title': text, 'role': 'button', 'tabindex': '0' }).keydown(function (e) { // 32 - space, 13 - enter if ([32, 13].indexOf(e.which) !== -1) { $(this).click(); e.preventDefault(); } }); $('<span>', { 'class': 'joubel-simple-rounded-button-text', 'html': text }).appendTo($simpleRoundedButton); return $simpleRoundedButton; } return SimpleRoundedButton; }(H5P.jQuery)); ; var H5P = H5P || {}; /** * Class responsible for creating speech bubbles */ H5P.JoubelSpeechBubble = (function ($) { var $currentSpeechBubble; var $currentContainer; var $tail; var $innerTail; var removeSpeechBubbleTimeout; var currentMaxWidth; var DEFAULT_MAX_WIDTH = 400; var iDevice = navigator.userAgent.match(/iPod|iPhone|iPad/g) ? true : false; /** * Creates a new speech bubble * * @param {H5P.jQuery} $container The speaking object * @param {string} text The text to display * @param {number} maxWidth The maximum width of the bubble * @return {H5P.JoubelSpeechBubble} */ function JoubelSpeechBubble($container, text, maxWidth) { maxWidth = maxWidth || DEFAULT_MAX_WIDTH; currentMaxWidth = maxWidth; $currentContainer = $container; this.isCurrent = function ($tip) { return $tip.is($currentContainer); }; this.remove = function () { remove(); }; var fadeOutSpeechBubble = function ($speechBubble) { if (!$speechBubble) { return; } // Stop removing bubble clearTimeout(removeSpeechBubbleTimeout); $speechBubble.removeClass('show'); setTimeout(function () { if ($speechBubble) { $speechBubble.remove(); $speechBubble = undefined; } }, 500); }; if ($currentSpeechBubble !== undefined) { remove(); } var $h5pContainer = getH5PContainer($container); // Make sure we fade out old speech bubble fadeOutSpeechBubble($currentSpeechBubble); // Create bubble $tail = $('<div class="joubel-speech-bubble-tail"></div>'); $innerTail = $('<div class="joubel-speech-bubble-inner-tail"></div>'); var $innerBubble = $( '<div class="joubel-speech-bubble-inner">' + '<div class="joubel-speech-bubble-text">' + text + '</div>' + '</div>' ).prepend($innerTail); $currentSpeechBubble = $( '<div class="joubel-speech-bubble" aria-live="assertive">' ).append([$tail, $innerBubble]) .appendTo($h5pContainer); // Show speech bubble with transition setTimeout(function () { $currentSpeechBubble.addClass('show'); }, 0); position($currentSpeechBubble, $currentContainer, maxWidth, $tail, $innerTail); // Handle click to close H5P.$body.on('mousedown.speechBubble', handleOutsideClick); // Handle window resizing H5P.$window.on('resize', '', handleResize); // Handle clicks when inside IV which blocks bubbling. $container.parents('.h5p-dialog') .on('mousedown.speechBubble', handleOutsideClick); if (iDevice) { H5P.$body.css('cursor', 'pointer'); } return this; } // Remove speechbubble if it belongs to a dom element that is about to be hidden H5P.externalDispatcher.on('domHidden', function (event) { if ($currentSpeechBubble !== undefined && event.data.$dom.find($currentContainer).length !== 0) { remove(); } }); /** * Returns the closest h5p container for the given DOM element. * * @param {object} $container jquery element * @return {object} the h5p container (jquery element) */ function getH5PContainer($container) { var $h5pContainer = $container.closest('.h5p-frame'); // Check closest h5p frame first, then check for container in case there is no frame. if (!$h5pContainer.length) { $h5pContainer = $container.closest('.h5p-container'); } return $h5pContainer; } /** * Event handler that is called when the window is resized. */ function handleResize() { position($currentSpeechBubble, $currentContainer, currentMaxWidth, $tail, $innerTail); } /** * Repositions the speech bubble according to the position of the container. * * @param {object} $currentSpeechbubble the speech bubble that should be positioned * @param {object} $container the container to which the speech bubble should point * @param {number} maxWidth the maximum width of the speech bubble * @param {object} $tail the tail (the triangle that points to the referenced container) * @param {object} $innerTail the inner tail (the triangle that points to the referenced container) */ function position($currentSpeechBubble, $container, maxWidth, $tail, $innerTail) { var $h5pContainer = getH5PContainer($container); // Calculate offset between the button and the h5p frame var offset = getOffsetBetween($h5pContainer, $container); var direction = (offset.bottom > offset.top ? 'bottom' : 'top'); var tipWidth = offset.outerWidth * 0.9; // Var needs to be renamed to make sense var bubbleWidth = tipWidth > maxWidth ? maxWidth : tipWidth; var bubblePosition = getBubblePosition(bubbleWidth, offset); var tailPosition = getTailPosition(bubbleWidth, bubblePosition, offset, $container.width()); // Need to set font-size, since element is appended to body. // Using same font-size as parent. In that way it will grow accordingly // when resizing var fontSize = 16;//parseFloat($parent.css('font-size')); // Set width and position of speech bubble $currentSpeechBubble.css(bubbleCSS( direction, bubbleWidth, bubblePosition, fontSize )); var preparedTailCSS = tailCSS(direction, tailPosition); $tail.css(preparedTailCSS); $innerTail.css(preparedTailCSS); } /** * Static function for removing the speechbubble */ var remove = function () { H5P.$body.off('mousedown.speechBubble'); H5P.$window.off('resize', '', handleResize); $currentContainer.parents('.h5p-dialog').off('mousedown.speechBubble'); if (iDevice) { H5P.$body.css('cursor', ''); } if ($currentSpeechBubble !== undefined) { // Apply transition, then remove speech bubble $currentSpeechBubble.removeClass('show'); // Make sure we remove any old timeout before reassignment clearTimeout(removeSpeechBubbleTimeout); removeSpeechBubbleTimeout = setTimeout(function () { $currentSpeechBubble.remove(); $currentSpeechBubble = undefined; }, 500); } // Don't return false here. If the user e.g. clicks a button when the bubble is visible, // we want the bubble to disapear AND the button to receive the event }; /** * Remove the speech bubble and container reference */ function handleOutsideClick(event) { if (event.target === $currentContainer[0]) { return; // Button clicks are not outside clicks } remove(); // There is no current container when a container isn't clicked $currentContainer = undefined; } /** * Calculate position for speech bubble * * @param {number} bubbleWidth The width of the speech bubble * @param {object} offset * @return {object} Return position for the speech bubble */ function getBubblePosition(bubbleWidth, offset) { var bubblePosition = {}; var tailOffset = 9; var widthOffset = bubbleWidth / 2; // Calculate top position bubblePosition.top = offset.top + offset.innerHeight; // Calculate bottom position bubblePosition.bottom = offset.bottom + offset.innerHeight + tailOffset; // Calculate left position if (offset.left < widthOffset) { bubblePosition.left = 3; } else if ((offset.left + widthOffset) > offset.outerWidth) { bubblePosition.left = offset.outerWidth - bubbleWidth - 3; } else { bubblePosition.left = offset.left - widthOffset + (offset.innerWidth / 2); } return bubblePosition; } /** * Calculate position for speech bubble tail * * @param {number} bubbleWidth The width of the speech bubble * @param {object} bubblePosition Speech bubble position * @param {object} offset * @param {number} iconWidth The width of the tip icon * @return {object} Return position for the tail */ function getTailPosition(bubbleWidth, bubblePosition, offset, iconWidth) { var tailPosition = {}; // Magic numbers. Tuned by hand so that the tail fits visually within // the bounds of the speech bubble. var leftBoundary = 9; var rightBoundary = bubbleWidth - 20; tailPosition.left = offset.left - bubblePosition.left + (iconWidth / 2) - 6; if (tailPosition.left < leftBoundary) { tailPosition.left = leftBoundary; } if (tailPosition.left > rightBoundary) { tailPosition.left = rightBoundary; } tailPosition.top = -6; tailPosition.bottom = -6; return tailPosition; } /** * Return bubble CSS for the desired growth direction * * @param {string} direction The direction the speech bubble will grow * @param {number} width The width of the speech bubble * @param {object} position Speech bubble position * @param {number} fontSize The size of the bubbles font * @return {object} Return CSS */ function bubbleCSS(direction, width, position, fontSize) { if (direction === 'top') { return { width: width + 'px', bottom: position.bottom + 'px', left: position.left + 'px', fontSize: fontSize + 'px', top: '' }; } else { return { width: width + 'px', top: position.top + 'px', left: position.left + 'px', fontSize: fontSize + 'px', bottom: '' }; } } /** * Return tail CSS for the desired growth direction * * @param {string} direction The direction the speech bubble will grow * @param {object} position Tail position * @return {object} Return CSS */ function tailCSS(direction, position) { if (direction === 'top') { return { bottom: position.bottom + 'px', left: position.left + 'px', top: '' }; } else { return { top: position.top + 'px', left: position.left + 'px', bottom: '' }; } } /** * Calculates the offset between an element inside a container and the * container. Only works if all the edges of the inner element are inside the * outer element. * Width/height of the elements is included as a convenience. * * @param {H5P.jQuery} $outer * @param {H5P.jQuery} $inner * @return {object} Position offset */ function getOffsetBetween($outer, $inner) { var outer = $outer[0].getBoundingClientRect(); var inner = $inner[0].getBoundingClientRect(); return { top: inner.top - outer.top, right: outer.right - inner.right, bottom: outer.bottom - inner.bottom, left: inner.left - outer.left, innerWidth: inner.width, innerHeight: inner.height, outerWidth: outer.width, outerHeight: outer.height }; } return JoubelSpeechBubble; })(H5P.jQuery); ; var H5P = H5P || {}; H5P.JoubelThrobber = (function ($) { /** * Creates a new tip */ function JoubelThrobber() { // h5p-throbber css is described in core var $throbber = $('<div/>', { 'class': 'h5p-throbber' }); return $throbber; } return JoubelThrobber; }(H5P.jQuery)); ; H5P.JoubelTip = (function ($) { var $conv = $('<div/>'); /** * Creates a new tip element. * * NOTE that this may look like a class but it doesn't behave like one. * It returns a jQuery object. * * @param {string} tipHtml The text to display in the popup * @param {Object} [behaviour] Options * @param {string} [behaviour.tipLabel] Set to use a custom label for the tip button (you want this for good A11Y) * @param {boolean} [behaviour.helpIcon] Set to 'true' to Add help-icon classname to Tip button (changes the icon) * @param {boolean} [behaviour.showSpeechBubble] Set to 'false' to disable functionality (you may this in the editor) * @param {boolean} [behaviour.tabcontrol] Set to 'true' if you plan on controlling the tabindex in the parent (tabindex="-1") * @return {H5P.jQuery|undefined} Tip button jQuery element or 'undefined' if invalid tip */ function JoubelTip(tipHtml, behaviour) { // Keep track of the popup that appears when you click the Tip button var speechBubble; // Parse tip html to determine text var tipText = $conv.html(tipHtml).text().trim(); if (tipText === '') { return; // The tip has no textual content, i.e. it's invalid. } // Set default behaviour behaviour = $.extend({ tipLabel: tipText, helpIcon: false, showSpeechBubble: true, tabcontrol: false }, behaviour); // Create Tip button var $tipButton = $('<div/>', { class: 'joubel-tip-container' + (behaviour.showSpeechBubble ? '' : ' be-quiet'), 'aria-label': behaviour.tipLabel, 'aria-expanded': false, role: 'button', tabindex: (behaviour.tabcontrol ? -1 : 0), click: function (event) { // Toggle show/hide popup toggleSpeechBubble(); event.preventDefault(); }, keydown: function (event) { if (event.which === 32 || event.which === 13) { // Space & enter key // Toggle show/hide popup toggleSpeechBubble(); event.stopPropagation(); event.preventDefault(); } else { // Any other key // Toggle hide popup toggleSpeechBubble(false); } }, // Add markup to render icon html: '<span class="joubel-icon-tip-normal ' + (behaviour.helpIcon ? ' help-icon': '') + '">' + '<span class="h5p-icon-shadow"></span>' + '<span class="h5p-icon-speech-bubble"></span>' + '<span class="h5p-icon-info"></span>' + '</span>' // IMPORTANT: All of the markup elements must have 'pointer-events: none;' }); const $tipAnnouncer = $('<div>', { 'class': 'hidden-but-read', 'aria-live': 'polite', appendTo: $tipButton, }); /** * Tip button interaction handler. * Toggle show or hide the speech bubble popup when interacting with the * Tip button. * * @private * @param {boolean} [force] 'true' shows and 'false' hides. */ var toggleSpeechBubble = function (force) { if (speechBubble !== undefined && speechBubble.isCurrent($tipButton)) { // Hide current popup speechBubble.remove(); speechBubble = undefined; $tipButton.attr('aria-expanded', false); $tipAnnouncer.html(''); } else if (force !== false && behaviour.showSpeechBubble) { // Create and show new popup speechBubble = H5P.JoubelSpeechBubble($tipButton, tipHtml); $tipButton.attr('aria-expanded', true); $tipAnnouncer.html(tipHtml); } }; return $tipButton; } return JoubelTip; })(H5P.jQuery); ; var H5P = H5P || {}; H5P.JoubelSlider = (function ($) { /** * Creates a new Slider * * @param {object} [params] Additional parameters */ function JoubelSlider(params) { H5P.EventDispatcher.call(this); this.$slider = $('<div>', $.extend({ 'class': 'h5p-joubel-ui-slider' }, params)); this.$slides = []; this.currentIndex = 0; this.numSlides = 0; } JoubelSlider.prototype = Object.create(H5P.EventDispatcher.prototype); JoubelSlider.prototype.constructor = JoubelSlider; JoubelSlider.prototype.addSlide = function ($content) { $content.addClass('h5p-joubel-ui-slide').css({ 'left': (this.numSlides*100) + '%' }); this.$slider.append($content); this.$slides.push($content); this.numSlides++; if(this.numSlides === 1) { $content.addClass('current'); } }; JoubelSlider.prototype.attach = function ($container) { $container.append(this.$slider); }; JoubelSlider.prototype.move = function (index) { var self = this; if(index === 0) { self.trigger('first-slide'); } if(index+1 === self.numSlides) { self.trigger('last-slide'); } self.trigger('move'); var $previousSlide = self.$slides[this.currentIndex]; H5P.Transition.onTransitionEnd(this.$slider, function () { $previousSlide.removeClass('current'); self.trigger('moved'); }); this.$slides[index].addClass('current'); var translateX = 'translateX(' + (-index*100) + '%)'; this.$slider.css({ '-webkit-transform': translateX, '-moz-transform': translateX, '-ms-transform': translateX, 'transform': translateX }); this.currentIndex = index; }; JoubelSlider.prototype.remove = function () { this.$slider.remove(); }; JoubelSlider.prototype.next = function () { if(this.currentIndex+1 >= this.numSlides) { return; } this.move(this.currentIndex+1); }; JoubelSlider.prototype.previous = function () { this.move(this.currentIndex-1); }; JoubelSlider.prototype.first = function () { this.move(0); }; JoubelSlider.prototype.last = function () { this.move(this.numSlides-1); }; return JoubelSlider; })(H5P.jQuery); ; var H5P = H5P || {}; /** * @module */ H5P.JoubelScoreBar = (function ($) { /* Need to use an id for the star SVG since that is the only way to reference SVG filters */ var idCounter = 0; /** * Creates a score bar * @class H5P.JoubelScoreBar * @param {number} maxScore Maximum score * @param {string} [label] Makes it easier for readspeakers to identify the scorebar * @param {string} [helpText] Score explanation * @param {string} [scoreExplanationButtonLabel] Label for score explanation button */ function JoubelScoreBar(maxScore, label, helpText, scoreExplanationButtonLabel) { var self = this; self.maxScore = maxScore; self.score = 0; idCounter++; /** * @const {string} */ self.STAR_MARKUP = '<svg tabindex="-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 63.77 53.87" aria-hidden="true" focusable="false">' + '<title>star</title>' + '<filter tabindex="-1" id="h5p-joubelui-score-bar-star-inner-shadow-' + idCounter + '" x0="-50%" y0="-50%" width="200%" height="200%">' + '<feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur"></feGaussianBlur>' + '<feOffset dy="2" dx="4"></feOffset>' + '<feComposite in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowDiff"></feComposite>' + '<feFlood flood-color="#ffe95c" flood-opacity="1"></feFlood>' + '<feComposite in2="shadowDiff" operator="in"></feComposite>' + '<feComposite in2="SourceGraphic" operator="over" result="firstfilter"></feComposite>' + '<feGaussianBlur in="firstfilter" stdDeviation="3" result="blur2"></feGaussianBlur>' + '<feOffset dy="-2" dx="-4"></feOffset>' + '<feComposite in2="firstfilter" operator="arithmetic" k2="-1" k3="1" result="shadowDiff"></feComposite>' + '<feFlood flood-color="#ffe95c" flood-opacity="1"></feFlood>' + '<feComposite in2="shadowDiff" operator="in"></feComposite>' + '<feComposite in2="firstfilter" operator="over"></feComposite>' + '</filter>' + '<path tabindex="-1" class="h5p-joubelui-score-bar-star-shadow" d="M35.08,43.41V9.16H20.91v0L9.51,10.85,9,10.93C2.8,12.18,0,17,0,21.25a11.22,11.22,0,0,0,3,7.48l8.73,8.53-1.07,6.16Z"/>' + '<g tabindex="-1">' + '<path tabindex="-1" class="h5p-joubelui-score-bar-star-border" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' + '<path tabindex="-1" class="h5p-joubelui-score-bar-star-fill" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' + '<path tabindex="-1" filter="url(#h5p-joubelui-score-bar-star-inner-shadow-' + idCounter + ')" class="h5p-joubelui-score-bar-star-fill-full-score" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' + '</g>' + '</svg>'; /** * @function appendTo * @memberOf H5P.JoubelScoreBar# * @param {H5P.jQuery} $wrapper Dom container */ self.appendTo = function ($wrapper) { self.$scoreBar.appendTo($wrapper); }; /** * Create the text representation of the scorebar . * * @private * @return {string} */ var createLabel = function (score) { if (!label) { return ''; } return label.replace(':num', score).replace(':total', self.maxScore); }; /** * Creates the html for this widget * * @method createHtml * @private */ var createHtml = function () { // Container div self.$scoreBar = $('<div>', { 'class': 'h5p-joubelui-score-bar', }); var $visuals = $('<div>', { 'class': 'h5p-joubelui-score-bar-visuals', appendTo: self.$scoreBar }); // The progress bar wrapper self.$progressWrapper = $('<div>', { 'class': 'h5p-joubelui-score-bar-progress-wrapper', appendTo: $visuals }); self.$progress = $('<div>', { 'class': 'h5p-joubelui-score-bar-progress', 'html': createLabel(self.score), appendTo: self.$progressWrapper }); // The star $('<div>', { 'class': 'h5p-joubelui-score-bar-star', html: self.STAR_MARKUP }).appendTo($visuals); // The score container var $numerics = $('<div>', { 'class': 'h5p-joubelui-score-numeric', appendTo: self.$scoreBar, 'aria-hidden': true }); // The current score self.$scoreCounter = $('<span>', { 'class': 'h5p-joubelui-score-number h5p-joubelui-score-number-counter', text: 0, appendTo: $numerics }); // The separator $('<span>', { 'class': 'h5p-joubelui-score-number-separator', text: '/', appendTo: $numerics }); // Max score self.$maxScore = $('<span>', { 'class': 'h5p-joubelui-score-number h5p-joubelui-score-max', text: self.maxScore, appendTo: $numerics }); if (helpText) { H5P.JoubelUI.createTip(helpText, { tipLabel: scoreExplanationButtonLabel ? scoreExplanationButtonLabel : helpText, helpIcon: true }).appendTo(self.$scoreBar); self.$scoreBar.addClass('h5p-score-bar-has-help'); } }; /** * Set the current score * @method setScore * @memberOf H5P.JoubelScoreBar# * @param {number} score */ self.setScore = function (score) { // Do nothing if score hasn't changed if (score === self.score) { return; } self.score = score > self.maxScore ? self.maxScore : score; self.updateVisuals(); }; /** * Increment score * @method incrementScore * @memberOf H5P.JoubelScoreBar# * @param {number=} incrementBy Optional parameter, defaults to 1 */ self.incrementScore = function (incrementBy) { self.setScore(self.score + (incrementBy || 1)); }; /** * Set the max score * @method setMaxScore * @memberOf H5P.JoubelScoreBar# * @param {number} maxScore The max score */ self.setMaxScore = function (maxScore) { self.maxScore = maxScore; }; /** * Updates the progressbar visuals * @memberOf H5P.JoubelScoreBar# * @method updateVisuals */ self.updateVisuals = function () { self.$progress.html(createLabel(self.score)); self.$scoreCounter.text(self.score); self.$maxScore.text(self.maxScore); setTimeout(function () { // Start the progressbar animation self.$progress.css({ width: ((self.score / self.maxScore) * 100) + '%' }); H5P.Transition.onTransitionEnd(self.$progress, function () { // If fullscore fill the star and start the animation self.$scoreBar.toggleClass('h5p-joubelui-score-bar-full-score', self.score === self.maxScore); self.$scoreBar.toggleClass('h5p-joubelui-score-bar-animation-active', self.score === self.maxScore); // Only allow the star animation to run once self.$scoreBar.one("animationend", function() { self.$scoreBar.removeClass("h5p-joubelui-score-bar-animation-active"); }); }, 600); }, 300); }; /** * Removes all classes * @method reset */ self.reset = function () { self.$scoreBar.removeClass('h5p-joubelui-score-bar-full-score'); }; createHtml(); } return JoubelScoreBar; })(H5P.jQuery); ; var H5P = H5P || {}; H5P.JoubelProgressbar = (function ($) { /** * Joubel progressbar class * @method JoubelProgressbar * @constructor * @param {number} steps Number of steps * @param {Object} [options] Additional options * @param {boolean} [options.disableAria] Disable readspeaker assistance * @param {string} [options.progressText] A progress text for describing * current progress out of total progress for readspeakers. * e.g. "Slide :num of :total" */ function JoubelProgressbar(steps, options) { H5P.EventDispatcher.call(this); var self = this; this.options = $.extend({ progressText: 'Slide :num of :total' }, options); this.currentStep = 0; this.steps = steps; this.$progressbar = $('<div>', { 'class': 'h5p-joubelui-progressbar' }); this.$background = $('<div>', { 'class': 'h5p-joubelui-progressbar-background' }).appendTo(this.$progressbar); } JoubelProgressbar.prototype = Object.create(H5P.EventDispatcher.prototype); JoubelProgressbar.prototype.constructor = JoubelProgressbar; JoubelProgressbar.prototype.updateAria = function () { var self = this; if (this.options.disableAria) { return; } if (!this.$currentStatus) { this.$currentStatus = $('<div>', { 'class': 'h5p-joubelui-progressbar-slide-status-text', 'aria-live': 'assertive' }).appendTo(this.$progressbar); } var interpolatedProgressText = self.options.progressText .replace(':num', self.currentStep) .replace(':total', self.steps); this.$currentStatus.html(interpolatedProgressText); }; /** * Appends to a container * @method appendTo * @param {H5P.jquery} $container */ JoubelProgressbar.prototype.appendTo = function ($container) { this.$progressbar.appendTo($container); }; /** * Update progress * @method setProgress * @param {number} step */ JoubelProgressbar.prototype.setProgress = function (step) { // Check for valid value: if (step > this.steps || step < 0) { return; } this.currentStep = step; this.$background.css({ width: ((this.currentStep/this.steps)*100) + '%' }); this.updateAria(); }; /** * Increment progress with 1 * @method next */ JoubelProgressbar.prototype.next = function () { this.setProgress(this.currentStep+1); }; /** * Reset progressbar * @method reset */ JoubelProgressbar.prototype.reset = function () { this.setProgress(0); }; /** * Check if last step is reached * @method isLastStep * @return {Boolean} */ JoubelProgressbar.prototype.isLastStep = function () { return this.steps === this.currentStep; }; return JoubelProgressbar; })(H5P.jQuery); ; var H5P = H5P || {}; /** * H5P Joubel UI library. * * This is a utility library, which does not implement attach. I.e, it has to bee actively used by * other libraries * @module */ H5P.JoubelUI = (function ($) { /** * The internal object to return * @class H5P.JoubelUI * @static */ function JoubelUI() {} /* Public static functions */ /** * Create a tip icon * @method H5P.JoubelUI.createTip * @param {string} text The textual tip * @param {Object} params Parameters * @return {H5P.JoubelTip} */ JoubelUI.createTip = function (text, params) { return new H5P.JoubelTip(text, params); }; /** * Create message dialog * @method H5P.JoubelUI.createMessageDialog * @param {H5P.jQuery} $container The dom container * @param {string} message The message * @return {H5P.JoubelMessageDialog} */ JoubelUI.createMessageDialog = function ($container, message) { return new H5P.JoubelMessageDialog($container, message); }; /** * Create help text dialog * @method H5P.JoubelUI.createHelpTextDialog * @param {string} header The textual header * @param {string} message The textual message * @param {string} closeButtonTitle The title for the close button * @return {H5P.JoubelHelpTextDialog} */ JoubelUI.createHelpTextDialog = function (header, message, closeButtonTitle) { return new H5P.JoubelHelpTextDialog(header, message, closeButtonTitle); }; /** * Create progress circle * @method H5P.JoubelUI.createProgressCircle * @param {number} number The progress (0 to 100) * @param {string} progressColor The progress color in hex value * @param {string} fillColor The fill color in hex value * @param {string} backgroundColor The background color in hex value * @return {H5P.JoubelProgressCircle} */ JoubelUI.createProgressCircle = function (number, progressColor, fillColor, backgroundColor) { return new H5P.JoubelProgressCircle(number, progressColor, fillColor, backgroundColor); }; /** * Create throbber for loading * @method H5P.JoubelUI.createThrobber * @return {H5P.JoubelThrobber} */ JoubelUI.createThrobber = function () { return new H5P.JoubelThrobber(); }; /** * Create simple rounded button * @method H5P.JoubelUI.createSimpleRoundedButton * @param {string} text The button label * @return {H5P.SimpleRoundedButton} */ JoubelUI.createSimpleRoundedButton = function (text) { return new H5P.SimpleRoundedButton(text); }; /** * Create Slider * @method H5P.JoubelUI.createSlider * @param {Object} [params] Parameters * @return {H5P.JoubelSlider} */ JoubelUI.createSlider = function (params) { return new H5P.JoubelSlider(params); }; /** * Create Score Bar * @method H5P.JoubelUI.createScoreBar * @param {number=} maxScore The maximum score * @param {string} [label] Makes it easier for readspeakers to identify the scorebar * @return {H5P.JoubelScoreBar} */ JoubelUI.createScoreBar = function (maxScore, label, helpText, scoreExplanationButtonLabel) { return new H5P.JoubelScoreBar(maxScore, label, helpText, scoreExplanationButtonLabel); }; /** * Create Progressbar * @method H5P.JoubelUI.createProgressbar * @param {number=} numSteps The total numer of steps * @param {Object} [options] Additional options * @param {boolean} [options.disableAria] Disable readspeaker assistance * @param {string} [options.progressText] A progress text for describing * current progress out of total progress for readspeakers. * e.g. "Slide :num of :total" * @return {H5P.JoubelProgressbar} */ JoubelUI.createProgressbar = function (numSteps, options) { return new H5P.JoubelProgressbar(numSteps, options); }; /** * Create standard Joubel button * * @method H5P.JoubelUI.createButton * @param {object} params * May hold any properties allowed by jQuery. If href is set, an A tag * is used, if not a button tag is used. * @return {H5P.jQuery} The jquery element created */ JoubelUI.createButton = function(params) { var type = 'button'; if (params.href) { type = 'a'; } else { params.type = 'button'; } if (params.class) { params.class += ' h5p-joubelui-button'; } else { params.class = 'h5p-joubelui-button'; } return $('<' + type + '/>', params); }; /** * Fix for iframe scoll bug in IOS. When focusing an element that doesn't have * focus support by default the iframe will scroll the parent frame so that * the focused element is out of view. This varies dependening on the elements * of the parent frame. */ if (H5P.isFramed && !H5P.hasiOSiframeScrollFix && /iPad|iPhone|iPod/.test(navigator.userAgent)) { H5P.hasiOSiframeScrollFix = true; // Keep track of original focus function var focus = HTMLElement.prototype.focus; // Override the original focus HTMLElement.prototype.focus = function () { // Only focus the element if it supports it natively if ( (this instanceof HTMLAnchorElement || this instanceof HTMLInputElement || this instanceof HTMLSelectElement || this instanceof HTMLTextAreaElement || this instanceof HTMLButtonElement || this instanceof HTMLIFrameElement || this instanceof HTMLAreaElement) && // HTMLAreaElement isn't supported by Safari yet. !this.getAttribute('role')) { // Focus breaks if a different role has been set // In theory this.isContentEditable should be able to recieve focus, // but it didn't work when tested. // Trigger the original focus with the proper context focus.call(this); } }; } return JoubelUI; })(H5P.jQuery); ; H5P.Tooltip = H5P.Tooltip || function() {}; H5P.Question = (function ($, EventDispatcher, JoubelUI) { /** * Extending this class make it alot easier to create tasks for other * content types. * * @class H5P.Question * @extends H5P.EventDispatcher * @param {string} type */ function Question(type) { var self = this; // Inheritance EventDispatcher.call(self); // Register default section order self.order = ['video', 'image', 'audio', 'introduction', 'content', 'explanation', 'feedback', 'scorebar', 'buttons', 'read']; // Keep track of registered sections var sections = {}; // Buttons var buttons = {}; var buttonOrder = []; // Wrapper when attached var $wrapper; // Click element var clickElement; // ScoreBar var scoreBar; // Keep track of the feedback's visual status. var showFeedback; // Keep track of which buttons are scheduled for hiding. var buttonsToHide = []; // Keep track of which buttons are scheduled for showing. var buttonsToShow = []; // Keep track of the hiding and showing of buttons. var toggleButtonsTimer; var toggleButtonsTransitionTimer; var buttonTruncationTimer; // Keeps track of initialization of question var initialized = false; /** * @type {Object} behaviour Behaviour of Question * @property {Boolean} behaviour.disableFeedback Set to true to disable feedback section */ var behaviour = { disableFeedback: false, disableReadSpeaker: false }; // Keeps track of thumb state var imageThumb = true; // Keeps track of image transitions var imageTransitionTimer; // Keep track of whether sections is transitioning. var sectionsIsTransitioning = false; // Keep track of auto play state var disableAutoPlay = false; // Feedback transition timer var feedbackTransitionTimer; // Used when reading messages to the user var $read, readText; /** * Register section with given content. * * @private * @param {string} section ID of the section * @param {(string|H5P.jQuery)} [content] */ var register = function (section, content) { sections[section] = {}; var $e = sections[section].$element = $('<div/>', { 'class': 'h5p-question-' + section, }); if (content) { $e[content instanceof $ ? 'append' : 'html'](content); } }; /** * Update registered section with content. * * @private * @param {string} section ID of the section * @param {(string|H5P.jQuery)} content */ var update = function (section, content) { if (content instanceof $) { sections[section].$element.html('').append(content); } else { sections[section].$element.html(content); } }; /** * Insert element with given ID into the DOM. * * @private * @param {array|Array|string[]} order * List with ordered element IDs * @param {string} id * ID of the element to be inserted * @param {Object} elements * Maps ID to the elements * @param {H5P.jQuery} $container * Parent container of the elements */ var insert = function (order, id, elements, $container) { // Try to find an element id should be after for (var i = 0; i < order.length; i++) { if (order[i] === id) { // Found our pos while (i > 0 && (elements[order[i - 1]] === undefined || !elements[order[i - 1]].isVisible)) { i--; } if (i === 0) { // We are on top. elements[id].$element.prependTo($container); } else { // Add after element elements[id].$element.insertAfter(elements[order[i - 1]].$element); } elements[id].isVisible = true; break; } } }; /** * Make feedback into a popup and position relative to click. * * @private * @param {string} [closeText] Text for the close button */ var makeFeedbackPopup = function (closeText) { var $element = sections.feedback.$element; var $parent = sections.content.$element; var $click = (clickElement != null ? clickElement.$element : null); $element.appendTo($parent).addClass('h5p-question-popup'); if (sections.scorebar) { sections.scorebar.$element.appendTo($element); } $parent.addClass('h5p-has-question-popup'); // Draw the tail var $tail = $('<div/>', { 'class': 'h5p-question-feedback-tail' }).hide() .appendTo($parent); // Draw the close button var $close = $('<div/>', { 'class': 'h5p-question-feedback-close', 'tabindex': 0, 'title': closeText, on: { click: function (event) { $element.remove(); $tail.remove(); event.preventDefault(); }, keydown: function (event) { switch (event.which) { case 13: // Enter case 32: // Space $element.remove(); $tail.remove(); event.preventDefault(); } } } }).hide().appendTo($element); if ($click != null) { if ($click.hasClass('correct')) { $element.addClass('h5p-question-feedback-correct'); $close.show(); sections.buttons.$element.hide(); } else { sections.buttons.$element.appendTo(sections.feedback.$element); } } positionFeedbackPopup($element, $click); }; /** * Position the feedback popup. * * @private * @param {H5P.jQuery} $element Feedback div * @param {H5P.jQuery} $click Visual click div */ var positionFeedbackPopup = function ($element, $click) { var $container = $element.parent(); var $tail = $element.siblings('.h5p-question-feedback-tail'); var popupWidth = $element.outerWidth(); var popupHeight = setElementHeight($element); var space = 15; var disableTail = false; var positionY = $container.height() / 2 - popupHeight / 2; var positionX = $container.width() / 2 - popupWidth / 2; var tailX = 0; var tailY = 0; var tailRotation = 0; if ($click != null) { // Edge detection for click, takes space into account var clickNearTop = ($click[0].offsetTop < space); var clickNearBottom = ($click[0].offsetTop + $click.height() > $container.height() - space); var clickNearLeft = ($click[0].offsetLeft < space); var clickNearRight = ($click[0].offsetLeft + $click.width() > $container.width() - space); // Click is not in a corner or close to edge, calculate position normally positionX = $click[0].offsetLeft - popupWidth / 2 + $click.width() / 2; positionY = $click[0].offsetTop - popupHeight - space; tailX = positionX + popupWidth / 2 - $tail.width() / 2; tailY = positionY + popupHeight - ($tail.height() / 2); tailRotation = 225; // If popup is outside top edge, position under click instead if (popupHeight + space > $click[0].offsetTop) { positionY = $click[0].offsetTop + $click.height() + space; tailY = positionY - $tail.height() / 2 ; tailRotation = 45; } // If popup is outside left edge, position left if (positionX < 0) { positionX = 0; } // If popup is outside right edge, position right if (positionX + popupWidth > $container.width()) { positionX = $container.width() - popupWidth; } // Special cases such as corner clicks, or close to an edge, they override X and Y positions if met if (clickNearTop && (clickNearLeft || clickNearRight)) { positionX = $click[0].offsetLeft + (clickNearLeft ? $click.width() : -popupWidth); positionY = $click[0].offsetTop + $click.height(); disableTail = true; } else if (clickNearBottom && (clickNearLeft || clickNearRight)) { positionX = $click[0].offsetLeft + (clickNearLeft ? $click.width() : -popupWidth); positionY = $click[0].offsetTop - popupHeight; disableTail = true; } else if (!clickNearTop && !clickNearBottom) { if (clickNearLeft || clickNearRight) { positionY = $click[0].offsetTop - popupHeight / 2 + $click.width() / 2; positionX = $click[0].offsetLeft + (clickNearLeft ? $click.width() + space : -popupWidth + -space); // Make sure this does not position the popup off screen if (positionX < 0) { positionX = 0; disableTail = true; } else { tailX = positionX + (clickNearLeft ? - $tail.width() / 2 : popupWidth - $tail.width() / 2); tailY = positionY + popupHeight / 2 - $tail.height() / 2; tailRotation = (clickNearLeft ? 315 : 135); } } } // Contain popup from overflowing bottom edge if (positionY + popupHeight > $container.height()) { positionY = $container.height() - popupHeight; if (popupHeight > $container.height() - ($click[0].offsetTop + $click.height() + space)) { disableTail = true; } } } else { disableTail = true; } // Contain popup from ovreflowing top edge if (positionY < 0) { positionY = 0; } $element.css({top: positionY, left: positionX}); $tail.css({top: tailY, left: tailX}); if (!disableTail) { $tail.css({ 'left': tailX, 'top': tailY, 'transform': 'rotate(' + tailRotation + 'deg)' }).show(); } else { $tail.hide(); } }; /** * Set element max height, used for animations. * * @param {H5P.jQuery} $element */ var setElementHeight = function ($element) { if (!$element.is(':visible')) { // No animation $element.css('max-height', 'none'); return; } // If this element is shown in the popup, we can't set width to 100%, // since it already has a width set in CSS var isFeedbackPopup = $element.hasClass('h5p-question-popup'); // Get natural element height var $tmp = $element.clone() .css({ 'position': 'absolute', 'max-height': 'none', 'width': isFeedbackPopup ? '' : '100%' }) .appendTo($element.parent()); // Need to take margins into account when calculating available space var sideMargins = parseFloat($element.css('margin-left')) + parseFloat($element.css('margin-right')); var tmpElWidth = $tmp.css('width') ? $tmp.css('width') : '100%'; $tmp.css('width', 'calc(' + tmpElWidth + ' - ' + sideMargins + 'px)'); // Apply height to element var h = Math.round($tmp.get(0).getBoundingClientRect().height); var fontSize = parseFloat($element.css('fontSize')); var relativeH = h / fontSize; $element.css('max-height', relativeH + 'em'); $tmp.remove(); if (h > 0 && sections.buttons && sections.buttons.$element === $element) { // Make sure buttons section is visible showSection(sections.buttons); // Resize buttons after resizing button section setTimeout(resizeButtons, 150); } return h; }; /** * Does the actual job of hiding the buttons scheduled for hiding. * * @private * @param {boolean} [relocateFocus] Find a new button to focus */ var hideButtons = function (relocateFocus) { for (var i = 0; i < buttonsToHide.length; i++) { hideButton(buttonsToHide[i].id); } buttonsToHide = []; if (relocateFocus) { self.focusButton(); } }; /** * Does the actual hiding. * @private * @param {string} buttonId */ var hideButton = function (buttonId) { // Using detach() vs hide() makes it harder to cheat. buttons[buttonId].$element.detach(); buttons[buttonId].isVisible = false; }; /** * Shows the buttons on the next tick. This is to avoid buttons flickering * If they're both added and removed on the same tick. * * @private */ var toggleButtons = function () { // If no buttons section, return if (sections.buttons === undefined) { return; } // Clear transition timer, reevaluate if buttons will be detached clearTimeout(toggleButtonsTransitionTimer); // Show buttons for (var i = 0; i < buttonsToShow.length; i++) { insert(buttonOrder, buttonsToShow[i].id, buttons, sections.buttons.$element); buttons[buttonsToShow[i].id].isVisible = true; } buttonsToShow = []; // Hide buttons var numToHide = 0; var relocateFocus = false; for (var j = 0; j < buttonsToHide.length; j++) { var button = buttons[buttonsToHide[j].id]; if (button.isVisible) { numToHide += 1; } if (button.$element.is(':focus')) { // Move focus to the first visible button. relocateFocus = true; } } var animationTimer = 150; if (sections.feedback && sections.feedback.$element.hasClass('h5p-question-popup')) { animationTimer = 0; } if (numToHide === sections.buttons.$element.children().length) { // All buttons are going to be hidden. Hide container using transition. hideSection(sections.buttons); // Detach buttons hideButtons(relocateFocus); } else { hideButtons(relocateFocus); // Show button section if (!sections.buttons.$element.is(':empty')) { showSection(sections.buttons); setElementHeight(sections.buttons.$element); // Trigger resize after animation toggleButtonsTransitionTimer = setTimeout(function () { self.trigger('resize'); }, animationTimer); } // Resize buttons to fit container resizeButtons(); } toggleButtonsTimer = undefined; }; /** * Allows for scaling of the question image. */ var scaleImage = function () { var $imgSection = sections.image.$element; clearTimeout(imageTransitionTimer); // Add this here to avoid initial transition of the image making // content overflow. Alternatively we need to trigger a resize. $imgSection.addClass('animatable'); if (imageThumb) { // Expand image $(this).attr('aria-expanded', true); $imgSection.addClass('h5p-question-image-fill-width'); imageThumb = false; imageTransitionTimer = setTimeout(function () { self.trigger('resize'); }, 600); } else { // Scale down image $(this).attr('aria-expanded', false); $imgSection.removeClass('h5p-question-image-fill-width'); imageThumb = true; imageTransitionTimer = setTimeout(function () { self.trigger('resize'); }, 600); } }; /** * Get scrollable ancestor of element * * @private * @param {H5P.jQuery} $element * @param {Number} [currDepth=0] Current recursive calls to ancestor, stop at maxDepth * @param {Number} [maxDepth=5] Maximum depth for finding ancestor. * @returns {H5P.jQuery} Parent element that is scrollable */ var findScrollableAncestor = function ($element, currDepth, maxDepth) { if (!currDepth) { currDepth = 0; } if (!maxDepth) { maxDepth = 5; } // Check validation of element or if we have reached document root if (!$element || !($element instanceof $) || document === $element.get(0) || currDepth >= maxDepth) { return; } if ($element.css('overflow-y') === 'auto') { return $element; } else { return findScrollableAncestor($element.parent(), currDepth + 1, maxDepth); } }; /** * Scroll to bottom of Question. * * @private */ var scrollToBottom = function () { if (!$wrapper || ($wrapper.hasClass('h5p-standalone') && !H5P.isFullscreen)) { return; // No scroll } var scrollableAncestor = findScrollableAncestor($wrapper); // Scroll to bottom of scrollable ancestor if (scrollableAncestor) { scrollableAncestor.animate({ scrollTop: $wrapper.css('height') }, "slow"); } }; /** * Resize buttons to fit container width * * @private */ var resizeButtons = function () { if (!buttons || !sections.buttons) { return; } var go = function () { // Don't do anything if button elements are not visible yet if (!sections.buttons.$element.is(':visible')) { return; } // Width of all buttons var buttonsWidth = { max: 0, min: 0, current: 0 }; for (var i in buttons) { var button = buttons[i]; if (button.isVisible) { setButtonWidth(buttons[i]); buttonsWidth.max += button.width.max; buttonsWidth.min += button.width.min; buttonsWidth.current += button.isTruncated ? button.width.min : button.width.max; } } var makeButtonsFit = function (availableWidth) { if (buttonsWidth.max < availableWidth) { // It is room for everyone on the right side of the score bar (without truncating) if (buttonsWidth.max !== buttonsWidth.current) { // Need to make everyone big restoreButtonLabels(buttonsWidth.current, availableWidth); } return true; } else if (buttonsWidth.min < availableWidth) { // Is it room for everyone on the right side of the score bar with truncating? if (buttonsWidth.current > availableWidth) { removeButtonLabels(buttonsWidth.current, availableWidth); } else { restoreButtonLabels(buttonsWidth.current, availableWidth); } return true; } return false; }; toggleFullWidthScorebar(false); var buttonSectionWidth = Math.floor(sections.buttons.$element.width()) - 1; if (!makeButtonsFit(buttonSectionWidth)) { // If we get here we need to wrap: toggleFullWidthScorebar(true); buttonSectionWidth = Math.floor(sections.buttons.$element.width()) - 1; makeButtonsFit(buttonSectionWidth); } }; // If visible, resize right away if (sections.buttons.$element.is(':visible')) { go(); } else { // If not visible, try on the next tick // Clear button truncation timer if within a button truncation function if (buttonTruncationTimer) { clearTimeout(buttonTruncationTimer); } buttonTruncationTimer = setTimeout(function () { buttonTruncationTimer = undefined; go(); }, 0); } }; var toggleFullWidthScorebar = function (enabled) { if (sections.scorebar && sections.scorebar.$element && sections.scorebar.$element.hasClass('h5p-question-visible')) { sections.buttons.$element.addClass('has-scorebar'); sections.buttons.$element.toggleClass('wrap', enabled); sections.scorebar.$element.toggleClass('full-width', enabled); } else { sections.buttons.$element.removeClass('has-scorebar'); } }; /** * Remove button labels until they use less than max width. * * @private * @param {Number} buttonsWidth Total width of all buttons * @param {Number} maxButtonsWidth Max width allowed for buttons */ var removeButtonLabels = function (buttonsWidth, maxButtonsWidth) { // Reverse traversal for (var i = buttonOrder.length - 1; i >= 0; i--) { var buttonId = buttonOrder[i]; var button = buttons[buttonId]; if (!button.isTruncated && button.isVisible) { var $button = button.$element; buttonsWidth -= button.width.max - button.width.min; // Set tooltip (needed by H5P.Tooltip) let buttonText = $button.text(); $button.attr('data-tooltip', buttonText); // Use button text as aria label if a specific one isn't provided if (!button.ariaLabel) { $button.attr('aria-label', buttonText); } // Remove label $button.html('').addClass('truncated'); button.isTruncated = true; if (buttonsWidth <= maxButtonsWidth) { // Buttons are small enough. return; } } } }; /** * Restore button labels until it fills maximum possible width without exceeding the max width. * * @private * @param {Number} buttonsWidth Total width of all buttons * @param {Number} maxButtonsWidth Max width allowed for buttons */ var restoreButtonLabels = function (buttonsWidth, maxButtonsWidth) { for (var i = 0; i < buttonOrder.length; i++) { var buttonId = buttonOrder[i]; var button = buttons[buttonId]; if (button.isTruncated && button.isVisible) { // Calculate new total width of buttons with a static pixel for consistency cross-browser buttonsWidth += button.width.max - button.width.min + 1; if (buttonsWidth > maxButtonsWidth) { return; } // Restore label button.$element.html(button.text); // Remove tooltip (used by H5P.Tooltip) button.$element.removeAttr('data-tooltip'); // Remove aria-label if a specific one isn't provided if (!button.ariaLabel) { button.$element.removeAttr('aria-label'); } button.$element.removeClass('truncated'); button.isTruncated = false; } } }; /** * Helper function for finding index of keyValue in array * * @param {String} keyValue Value to be found * @param {String} key In key * @param {Array} array In array * @returns {number} */ var existsInArray = function (keyValue, key, array) { var i; for (i = 0; i < array.length; i++) { if (array[i][key] === keyValue) { return i; } } return -1; }; /** * Show a section * @param {Object} section */ var showSection = function (section) { section.$element.addClass('h5p-question-visible'); section.isVisible = true; }; /** * Hide a section * @param {Object} section */ var hideSection = function (section) { section.$element.css('max-height', ''); section.isVisible = false; setTimeout(function () { // Only hide if section hasn't been set to visible in the meantime if (!section.isVisible) { section.$element.removeClass('h5p-question-visible'); } }, 150); }; /** * Set behaviour for question. * * @param {Object} options An object containing behaviour that will be extended by Question */ self.setBehaviour = function (options) { $.extend(behaviour, options); }; /** * A video to display above the task. * * @param {object} params */ self.setVideo = function (params) { sections.video = { $element: $('<div/>', { 'class': 'h5p-question-video' }) }; if (disableAutoPlay && params.params.playback) { params.params.playback.autoplay = false; } // Never fit to wrapper if (!params.params.visuals) { params.params.visuals = {}; } params.params.visuals.fit = false; sections.video.instance = H5P.newRunnable(params, self.contentId, sections.video.$element, true); var fromVideo = false; // Hack to avoid never ending loop sections.video.instance.on('resize', function () { fromVideo = true; self.trigger('resize'); fromVideo = false; }); self.on('resize', function () { if (!fromVideo) { sections.video.instance.trigger('resize'); } }); return self; }; /** * An audio player to display above the task. * * @param {object} params */ self.setAudio = function (params) { params.params = params.params || {}; sections.audio = { $element: $('<div/>', { 'class': 'h5p-question-audio', }) }; if (disableAutoPlay) { params.params.autoplay = false; } else if (params.params.playerMode === 'transparent') { params.params.autoplay = true; // false doesn't make sense for transparent audio } sections.audio.instance = H5P.newRunnable(params, self.contentId, sections.audio.$element, true); // The height value that is set by H5P.Audio is counter-productive here. if (sections.audio.instance.audio) { sections.audio.instance.audio.style.height = ''; } return self; }; /** * Will stop any playback going on in the task. */ self.pause = function () { if (sections.video && sections.video.isVisible) { sections.video.instance.pause(); } if (sections.audio && sections.audio.isVisible) { sections.audio.instance.pause(); } }; /** * Start playback of video */ self.play = function () { if (sections.video && sections.video.isVisible) { sections.video.instance.play(); } if (sections.audio && sections.audio.isVisible) { sections.audio.instance.play(); } }; /** * Disable auto play, useful in editors. */ self.disableAutoPlay = function () { disableAutoPlay = true; }; /** * Process HTML escaped string for use as attribute value, * e.g. for alt text or title attributes. * * @param {string} value * @return {string} WARNING! Do NOT use for innerHTML. */ self.massageAttributeOutput = function (value) { const dparser = new DOMParser().parseFromString(value, 'text/html'); const div = document.createElement('div'); div.innerHTML = dparser.documentElement.textContent;; return div.textContent || div.innerText || ''; }; /** * Add task image. * * @param {string} path Relative * @param {Object} [options] Options object * @param {string} [options.alt] Text representation * @param {string} [options.title] Hover text * @param {Boolean} [options.disableImageZooming] Set as true to disable image zooming * @param {string} [options.expandImage] Localization strings * @param {string} [options.minimizeImage] Localization string */ self.setImage = function (path, options) { options = options ? options : {}; sections.image = {}; // Image container sections.image.$element = $('<div/>', { 'class': 'h5p-question-image h5p-question-image-fill-width' }); // Inner wrap var $imgWrap = $('<div/>', { 'class': 'h5p-question-image-wrap', appendTo: sections.image.$element }); // Image element var $img = $('<img/>', { src: H5P.getPath(path, self.contentId), alt: (options.alt === undefined ? '' : self.massageAttributeOutput(options.alt)), title: (options.title === undefined ? '' : self.massageAttributeOutput(options.title)), on: { load: function () { self.trigger('imageLoaded', this); self.trigger('resize'); } }, appendTo: $imgWrap }); // Disable image zooming if (options.disableImageZooming) { $img.css('maxHeight', 'none'); // Make sure we are using the correct amount of width at all times var determineImgWidth = function () { // Remove margins if natural image width is bigger than section width var imageSectionWidth = sections.image.$element.get(0).getBoundingClientRect().width; // Do not transition, for instant measurements $imgWrap.css({ '-webkit-transition': 'none', 'transition': 'none' }); // Margin as translateX on both sides of image. var diffX = 2 * ($imgWrap.get(0).getBoundingClientRect().left - sections.image.$element.get(0).getBoundingClientRect().left); if ($img.get(0).naturalWidth >= imageSectionWidth - diffX) { sections.image.$element.addClass('h5p-question-image-fill-width'); } else { // Use margin for small res images sections.image.$element.removeClass('h5p-question-image-fill-width'); } // Reset transition rules $imgWrap.css({ '-webkit-transition': '', 'transition': '' }); }; // Determine image width if ($img.is(':visible')) { determineImgWidth(); } else { $img.on('load', determineImgWidth); } // Skip adding zoom functionality return; } const setAriaLabel = () => { const ariaLabel = $imgWrap.attr('aria-expanded') === 'true' ? options.minimizeImage : options.expandImage; $imgWrap.attr('aria-label', `${ariaLabel} ${options.alt}`); }; var sizeDetermined = false; var determineSize = function () { if (sizeDetermined || !$img.is(':visible')) { return; // Try again next time. } $imgWrap.addClass('h5p-question-image-scalable') .attr('aria-expanded', false) .attr('role', 'button') .attr('tabIndex', '0') .on('click', function (event) { if (event.which === 1) { scaleImage.apply(this); // Left mouse button click setAriaLabel(); } }).on('keypress', function (event) { if (event.which === 32) { event.preventDefault(); // Prevent default behaviour; page scroll down scaleImage.apply(this); // Space bar pressed setAriaLabel(); } }); setAriaLabel(); sections.image.$element.removeClass('h5p-question-image-fill-width'); sizeDetermined = true; // Prevent any futher events }; self.on('resize', determineSize); return self; }; /** * Add the introduction section. * * @param {(string|H5P.jQuery)} content */ self.setIntroduction = function (content) { register('introduction', content); return self; }; /** * Add the content section. * * @param {(string|H5P.jQuery)} content * @param {Object} [options] * @param {string} [options.class] */ self.setContent = function (content, options) { register('content', content); if (options && options.class) { sections.content.$element.addClass(options.class); } return self; }; /** * Force readspeaker to read text. Useful when you have to use * setTimeout for animations. */ self.read = function (content) { if (!$read) { return; // Not ready yet } if (readText) { // Combine texts if called multiple times readText += (readText.substr(-1, 1) === '.' ? ' ' : '. ') + content; } else { readText = content; } // Set text $read.html(readText); setTimeout(function () { // Stop combining when done reading readText = null; $read.html(''); }, 100); }; /** * Read feedback */ self.readFeedback = function () { var invalidFeedback = behaviour.disableReadSpeaker || !showFeedback || !sections.feedback || !sections.feedback.$element; if (invalidFeedback) { return; } var $feedbackText = $('.h5p-question-feedback-content-text', sections.feedback.$element); if ($feedbackText && $feedbackText.html() && $feedbackText.html().length) { self.read($feedbackText.html()); } }; /** * Remove feedback * * @return {H5P.Question} */ self.removeFeedback = function () { clearTimeout(feedbackTransitionTimer); if (sections.feedback && showFeedback) { showFeedback = false; // Hide feedback & scorebar hideSection(sections.scorebar); hideSection(sections.feedback); sectionsIsTransitioning = true; // Detach after transition feedbackTransitionTimer = setTimeout(function () { // Avoiding Transition.onTransitionEnd since it will register multiple events, and there's no way to cancel it if the transition changes back to "show" while the animation is happening. if (!showFeedback) { sections.feedback.$element.children().detach(); sections.scorebar.$element.children().detach(); // Trigger resize after animation self.trigger('resize'); } sectionsIsTransitioning = false; scoreBar.setScore(0); }, 150); if ($wrapper) { $wrapper.find('.h5p-question-feedback-tail').remove(); } } return self; }; /** * Set feedback message. * * @param {string} [content] * @param {number} score The score * @param {number} maxScore The maximum score for this question * @param {string} [scoreBarLabel] Makes it easier for readspeakers to identify the scorebar * @param {string} [helpText] Help text that describes the score inside a tip icon * @param {object} [popupSettings] Extra settings for popup feedback * @param {boolean} [popupSettings.showAsPopup] Should the feedback display as popup? * @param {string} [popupSettings.closeText] Translation for close button text * @param {object} [popupSettings.click] Element representing where user clicked on screen */ self.setFeedback = function (content, score, maxScore, scoreBarLabel, helpText, popupSettings, scoreExplanationButtonLabel) { // Feedback is disabled if (behaviour.disableFeedback) { return self; } // Need to toggle buttons right away to avoid flickering/blinking // Note: This means content types should invoke hide/showButton before setFeedback toggleButtons(); clickElement = (popupSettings != null && popupSettings.click != null ? popupSettings.click : null); clearTimeout(feedbackTransitionTimer); var $feedback = $('<div>', { 'class': 'h5p-question-feedback-container' }); var $feedbackContent = $('<div>', { 'class': 'h5p-question-feedback-content' }).appendTo($feedback); // Feedback text $('<div>', { 'class': 'h5p-question-feedback-content-text', 'html': content }).appendTo($feedbackContent); var $scorebar = $('<div>', { 'class': 'h5p-question-scorebar-container' }); if (scoreBar === undefined) { scoreBar = JoubelUI.createScoreBar(maxScore, scoreBarLabel, helpText, scoreExplanationButtonLabel); } scoreBar.appendTo($scorebar); $feedbackContent.toggleClass('has-content', content !== undefined && content.length > 0); // Feedback for readspeakers if (!behaviour.disableReadSpeaker && scoreBarLabel) { self.read(scoreBarLabel.replace(':num', score).replace(':total', maxScore) + '. ' + (content ? content : '')); } showFeedback = true; if (sections.feedback) { // Update section update('feedback', $feedback); update('scorebar', $scorebar); } else { // Create section register('feedback', $feedback); register('scorebar', $scorebar); if (initialized && $wrapper) { insert(self.order, 'feedback', sections, $wrapper); insert(self.order, 'scorebar', sections, $wrapper); } } showSection(sections.feedback); showSection(sections.scorebar); resizeButtons(); if (popupSettings != null && popupSettings.showAsPopup == true) { makeFeedbackPopup(popupSettings.closeText); scoreBar.setScore(score); } else { // Show feedback section feedbackTransitionTimer = setTimeout(function () { setElementHeight(sections.feedback.$element); setElementHeight(sections.scorebar.$element); sectionsIsTransitioning = true; // Scroll to bottom after showing feedback scrollToBottom(); // Trigger resize after animation feedbackTransitionTimer = setTimeout(function () { sectionsIsTransitioning = false; self.trigger('resize'); scoreBar.setScore(score); }, 150); }, 0); } return self; }; /** * Set feedback content (no animation). * * @param {string} content * @param {boolean} [extendContent] True will extend content, instead of replacing it */ self.updateFeedbackContent = function (content, extendContent) { if (sections.feedback && sections.feedback.$element) { if (extendContent) { content = $('.h5p-question-feedback-content', sections.feedback.$element).html() + ' ' + content; } // Update feedback content html $('.h5p-question-feedback-content', sections.feedback.$element).html(content).addClass('has-content'); // Make sure the height is correct setElementHeight(sections.feedback.$element); // Need to trigger resize when feedback has finished transitioning setTimeout(self.trigger.bind(self, 'resize'), 150); } return self; }; /** * Set the content of the explanation / feedback panel * * @param {Object} data * @param {string} data.correct * @param {string} data.wrong * @param {string} data.text * @param {string} title Title for explanation panel * * @return {H5P.Question} */ self.setExplanation = function (data, title) { if (data) { var explainer = new H5P.Question.Explainer(title, data); if (sections.explanation) { // Update section update('explanation', explainer.getElement()); } else { register('explanation', explainer.getElement()); if (initialized && $wrapper) { insert(self.order, 'explanation', sections, $wrapper); } } } else if (sections.explanation) { // Hide explanation section sections.explanation.$element.children().detach(); } return self; }; /** * Checks to see if button is registered. * * @param {string} id * @returns {boolean} */ self.hasButton = function (id) { return (buttons[id] !== undefined); }; /** * @typedef {Object} ConfirmationDialog * @property {boolean} [enable] Must be true to show confirmation dialog * @property {Object} [instance] Instance that uses confirmation dialog * @property {jQuery} [$parentElement] Append to this element. * @property {Object} [l10n] Translatable fields * @property {string} [l10n.header] Header text * @property {string} [l10n.body] Body text * @property {string} [l10n.cancelLabel] * @property {string} [l10n.confirmLabel] */ /** * Register buttons for the task. * * @param {string} id * @param {string} text label * @param {function} clicked * @param {boolean} [visible=true] * @param {Object} [options] Options for button * @param {Object} [extras] Extra options * @param {ConfirmationDialog} [extras.confirmationDialog] Confirmation dialog * @param {Object} [extras.contentData] Content data * @params {string} [extras.textIfSubmitting] Text to display if submitting */ self.addButton = function (id, text, clicked, visible, options, extras) { if (buttons[id]) { return self; // Already registered } if (sections.buttons === undefined) { // We have buttons, register wrapper register('buttons'); if (initialized) { insert(self.order, 'buttons', sections, $wrapper); } } extras = extras || {}; extras.confirmationDialog = extras.confirmationDialog || {}; options = options || {}; var confirmationDialog = self.addConfirmationDialogToButton(extras.confirmationDialog, clicked); /** * Handle button clicks through both mouse and keyboard * @private */ var handleButtonClick = function () { if (extras.confirmationDialog.enable && confirmationDialog) { // Show popups section if used if (!extras.confirmationDialog.$parentElement) { sections.popups.$element.removeClass('hidden'); } confirmationDialog.show($e.position().top); } else { clicked(); } }; const isSubmitting = extras.contentData && extras.contentData.standalone && (extras.contentData.isScoringEnabled || extras.contentData.isReportingEnabled); if (isSubmitting && extras.textIfSubmitting) { text = extras.textIfSubmitting; } buttons[id] = { isTruncated: false, text: text, isVisible: false, ariaLabel: options['aria-label'] }; // The button might be <button> or <a> // (dependent on options.href set or not) var isAnchorTag = (options.href !== undefined); var $e = buttons[id].$element = JoubelUI.createButton($.extend({ 'class': 'h5p-question-' + id, html: text, on: { click: function (event) { handleButtonClick(); if (isAnchorTag) { event.preventDefault(); } } } }, options)); buttonOrder.push(id); H5P.Tooltip($e.get(0), {tooltipSource: 'data-tooltip'}); // The button might be <button> or <a>. If <a>, the space key is not // triggering the click event, must therefore handle this here: if (isAnchorTag) { $e.on('keypress', function (event) { if (event.which === 32) { // Space handleButtonClick(); event.preventDefault(); } }); } if (visible === undefined || visible) { // Button should be visible $e.appendTo(sections.buttons.$element); buttons[id].isVisible = true; showSection(sections.buttons); } return self; }; var setButtonWidth = function (button) { var $button = button.$element; var $tmp = $button.clone() .css({ 'position': 'absolute', 'white-space': 'nowrap', 'max-width': 'none' }).removeClass('truncated') .html(button.text) .appendTo($button.parent()); // Calculate max width (button including text) button.width = { max: Math.ceil($tmp.outerWidth() + parseFloat($tmp.css('margin-left')) + parseFloat($tmp.css('margin-right'))) }; // Calculate min width (truncated, icon only) $tmp.html('').addClass('truncated'); button.width.min = Math.ceil($tmp.outerWidth() + parseFloat($tmp.css('margin-left')) + parseFloat($tmp.css('margin-right'))); $tmp.remove(); }; /** * Add confirmation dialog to button * @param {ConfirmationDialog} options * A confirmation dialog that will be shown before click handler of button * is triggered * @param {function} clicked * Click handler of button * @return {H5P.ConfirmationDialog|undefined} * Confirmation dialog if enabled */ self.addConfirmationDialogToButton = function (options, clicked) { options = options || {}; if (!options.enable) { return; } // Confirmation dialog var confirmationDialog = new H5P.ConfirmationDialog({ instance: options.instance, headerText: options.l10n.header, dialogText: options.l10n.body, cancelText: options.l10n.cancelLabel, confirmText: options.l10n.confirmLabel }); // Determine parent element if (options.$parentElement) { const parentElement = options.$parentElement.get(0); let dialogParent; // If using h5p-content, dialog will not appear on embedded fullscreen if (parentElement.classList.contains('h5p-content')) { dialogParent = parentElement.querySelector('.h5p-container'); } confirmationDialog.appendTo(dialogParent ?? parentElement); } else { // Create popup section and append to that if (sections.popups === undefined) { register('popups'); if (initialized) { insert(self.order, 'popups', sections, $wrapper); } sections.popups.$element.addClass('hidden'); self.order.push('popups'); } confirmationDialog.appendTo(sections.popups.$element.get(0)); } // Add event listeners confirmationDialog.on('confirmed', function () { if (!options.$parentElement) { sections.popups.$element.addClass('hidden'); } clicked(); // Trigger to content type self.trigger('confirmed'); }); confirmationDialog.on('canceled', function () { if (!options.$parentElement) { sections.popups.$element.addClass('hidden'); } // Trigger to content type self.trigger('canceled'); }); return confirmationDialog; }; /** * Show registered button with given identifier. * * @param {string} id * @param {Number} [priority] */ self.showButton = function (id, priority) { var aboutToBeHidden = existsInArray(id, 'id', buttonsToHide) !== -1; if (buttons[id] === undefined || (buttons[id].isVisible === true && !aboutToBeHidden)) { return self; } priority = priority || 0; // Skip if already being shown var indexToShow = existsInArray(id, 'id', buttonsToShow); if (indexToShow !== -1) { // Update priority if (buttonsToShow[indexToShow].priority < priority) { buttonsToShow[indexToShow].priority = priority; } return self; } // Check if button is going to be hidden on next tick var exists = existsInArray(id, 'id', buttonsToHide); if (exists !== -1) { // Skip hiding if higher priority if (buttonsToHide[exists].priority <= priority) { buttonsToHide.splice(exists, 1); buttonsToShow.push({id: id, priority: priority}); } } // If button is not shown else if (!buttons[id].$element.is(':visible')) { // Show button on next tick buttonsToShow.push({id: id, priority: priority}); } if (!toggleButtonsTimer) { toggleButtonsTimer = setTimeout(toggleButtons, 0); } return self; }; /** * Hide registered button with given identifier. * * @param {string} id * @param {number} [priority] */ self.hideButton = function (id, priority) { var aboutToBeShown = existsInArray(id, 'id', buttonsToShow) !== -1; if (buttons[id] === undefined || (buttons[id].isVisible === false && !aboutToBeShown)) { return self; } priority = priority || 0; // Skip if already being hidden var indexToHide = existsInArray(id, 'id', buttonsToHide); if (indexToHide !== -1) { // Update priority if (buttonsToHide[indexToHide].priority < priority) { buttonsToHide[indexToHide].priority = priority; } return self; } // Check if buttons is going to be shown on next tick var exists = existsInArray(id, 'id', buttonsToShow); if (exists !== -1) { // Skip showing if higher priority if (buttonsToShow[exists].priority <= priority) { buttonsToShow.splice(exists, 1); buttonsToHide.push({id: id, priority: priority}); } } else if (!buttons[id].$element.is(':visible')) { // Make sure it is detached in case the container is hidden. hideButton(id); } else { // Hide button on next tick. buttonsToHide.push({id: id, priority: priority}); } if (!toggleButtonsTimer) { toggleButtonsTimer = setTimeout(toggleButtons, 0); } return self; }; /** * Set focus to the given button. If no button is given the first visible * button gets focused. This is useful if you lose focus. * * @param {string} [id] */ self.focusButton = function (id) { if (id === undefined) { // Find first button that is visible. for (var i = 0; i < buttonOrder.length; i++) { var button = buttons[buttonOrder[i]]; if (button && button.isVisible) { // Give that button focus button.$element.focus(); break; } } } else if (buttons[id] && buttons[id].$element.is(':visible')) { // Set focus to requested button buttons[id].$element.focus(); } return self; }; /** * Toggle readspeaker functionality * @param {boolean} [disable] True to disable, false to enable. */ self.toggleReadSpeaker = function (disable) { behaviour.disableReadSpeaker = disable || !behaviour.disableReadSpeaker; }; /** * Set new element for section. * * @param {String} id * @param {H5P.jQuery} $element */ self.insertSectionAtElement = function (id, $element) { if (sections[id] === undefined) { register(id); } sections[id].parent = $element; // Insert section if question is not initialized if (!initialized) { insert([id], id, sections, $element); } return self; }; /** * Attach content to given container. * * @param {H5P.jQuery} $container */ self.attach = function ($container) { if (self.isRoot()) { self.setActivityStarted(); } // The first time we attach we also create our DOM elements. if ($wrapper === undefined) { if (self.registerDomElements !== undefined && (self.registerDomElements instanceof Function || typeof self.registerDomElements === 'function')) { // Give the question type a chance to register before attaching self.registerDomElements(); } // Create section for reading messages $read = $('<div/>', { 'aria-live': 'polite', 'class': 'h5p-hidden-read' }); register('read', $read); self.trigger('registerDomElements'); } // Prepare container $wrapper = $container; $container.html('') .addClass('h5p-question h5p-' + type); // Add sections in given order var $sections = []; for (var i = 0; i < self.order.length; i++) { var section = self.order[i]; if (sections[section]) { if (sections[section].parent) { // Section has a different parent sections[section].$element.appendTo(sections[section].parent); } else { $sections.push(sections[section].$element); } sections[section].isVisible = true; } } // Only append once to DOM for optimal performance $container.append($sections); // Let others react to dom changes self.trigger('domChanged', { '$target': $container, 'library': self.libraryInfo.machineName, 'contentId': self.contentId, 'key': 'newLibrary' }, {'bubbles': true, 'external': true}); // ?? initialized = true; return self; }; /** * Detach all sections from their parents */ self.detachSections = function () { // Deinit Question initialized = false; // Detach sections for (var section in sections) { sections[section].$element.detach(); } return self; }; // Listen for resize self.on('resize', function () { // Allow elements to attach and set their height before resizing if (!sectionsIsTransitioning && sections.feedback && showFeedback) { // Resize feedback to fit setElementHeight(sections.feedback.$element); } // Re-position feedback popup if in use var $element = sections.feedback; var $click = clickElement; if ($element != null && $element.$element != null && $click != null && $click.$element != null) { setTimeout(function () { positionFeedbackPopup($element.$element, $click.$element); }, 10); } resizeButtons(); }); } // Inheritance Question.prototype = Object.create(EventDispatcher.prototype); Question.prototype.constructor = Question; /** * Determine the overall feedback to display for the question. * Returns empty string if no matching range is found. * * @param {Object[]} feedbacks * @param {number} scoreRatio * @return {string} */ Question.determineOverallFeedback = function (feedbacks, scoreRatio) { scoreRatio = Math.floor(scoreRatio * 100); for (var i = 0; i < feedbacks.length; i++) { var feedback = feedbacks[i]; var hasFeedback = (feedback.feedback !== undefined && feedback.feedback.trim().length !== 0); if (feedback.from <= scoreRatio && feedback.to >= scoreRatio && hasFeedback) { return feedback.feedback; } } return ''; }; return Question; })(H5P.jQuery, H5P.EventDispatcher, H5P.JoubelUI); ; H5P.Question.Explainer = (function ($) { /** * Constructor * * @class * @param {string} title * @param {array} explanations */ function Explainer(title, explanations) { var self = this; /** * Create the DOM structure */ var createHTML = function () { self.$explanation = $('<div>', { 'class': 'h5p-question-explanation-container' }); // Add title: $('<div>', { 'class': 'h5p-question-explanation-title', role: 'heading', html: title, appendTo: self.$explanation }); var $explanationList = $('<ul>', { 'class': 'h5p-question-explanation-list', appendTo: self.$explanation }); for (var i = 0; i < explanations.length; i++) { var feedback = explanations[i]; var $explanationItem = $('<li>', { 'class': 'h5p-question-explanation-item', appendTo: $explanationList }); var $content = $('<div>', { 'class': 'h5p-question-explanation-status' }); if (feedback.correct) { $('<span>', { 'class': 'h5p-question-explanation-correct', html: feedback.correct, appendTo: $content }); } if (feedback.wrong) { $('<span>', { 'class': 'h5p-question-explanation-wrong', html: feedback.wrong, appendTo: $content }); } $content.appendTo($explanationItem); if (feedback.text) { $('<div>', { 'class': 'h5p-question-explanation-text', html: feedback.text, appendTo: $explanationItem }); } } }; createHTML(); /** * Return the container HTMLElement * * @return {HTMLElement} */ self.getElement = function () { return self.$explanation; }; } return Explainer; })(H5P.jQuery); ; (function (Question) { /** * Makes it easy to add animated score points for your question type. * * @class H5P.Question.ScorePoints */ Question.ScorePoints = function () { var self = this; var elements = []; var showElementsTimer; /** * Create the element that displays the score point element for questions. * * @param {boolean} isCorrect * @return {HTMLElement} */ self.getElement = function (isCorrect) { var element = document.createElement('div'); element.classList.add(isCorrect ? 'h5p-question-plus-one' : 'h5p-question-minus-one'); element.classList.add('h5p-question-hidden-one'); elements.push(element); // Schedule display animation of all added elements if (showElementsTimer) { clearTimeout(showElementsTimer); } showElementsTimer = setTimeout(showElements, 0); return element; }; /** * @private */ var showElements = function () { // Determine delay between triggering animations var delay = 0; var increment = 150; var maxTime = 1000; if (elements.length && elements.length > Math.ceil(maxTime / increment)) { // Animations will run for more than ~1 second, reduce it. increment = maxTime / elements.length; } for (var i = 0; i < elements.length; i++) { // Use timer to trigger show setTimeout(showElement(elements[i]), delay); // Increse delay for next element delay += increment; } }; /** * Trigger transition animation for the given element * * @private * @param {HTMLElement} element * @return {function} */ var showElement = function (element) { return function () { element.classList.remove('h5p-question-hidden-one'); }; }; }; })(H5P.Question); ; var H5P = H5P || {}; /** * H5P-Text Utilities * * Some functions that can be useful when dealing with texts in H5P. * * @param {H5P.jQuery} $ */ H5P.TextUtilities = function () { 'use strict'; /** * Create Text Utilities. * * Might be needed later. * * @constructor */ function TextUtilities () { } // Inheritance TextUtilities.prototype = Object.create(H5P.EventDispatcher.prototype); TextUtilities.prototype.constructor = TextUtilities; /** @constant {object} */ TextUtilities.WORD_DELIMITER = /[\s.?!,\';\"]/g; /** * Check if a candidate string is considered isolated (in a larger string) by * checking the symbol before and after the candidate. * * @param {string} candidate - String to be looked for. * @param {string} text - (Larger) string that should contain candidate. * @param {object} params - Parameters. * @param {object} params.delimiter - Regular expression containing symbols used to isolate the candidate. * @return {boolean} True if string is isolated. */ TextUtilities.isIsolated = function (candidate, text, params) { // Sanitization if (!candidate || !text) { return; } var delimiter = (!!params && !!params.delimiter) ? params.delimiter : TextUtilities.WORD_DELIMITER; var pos = (!!params && !!params.index && typeof params.index === 'number') ? params.index : text.indexOf(candidate); if (pos < 0 || pos > text.length-1) { return false; } var pred = (pos === 0 ? '' : text[pos - 1].replace(delimiter, '')); var succ = (pos + candidate.length === text.length ? '' : text[pos + candidate.length].replace(delimiter, '')); if (pred !== '' || succ !== '') { return false; } return true; }; /** * Check whether two strings are considered to be similar. * The similarity is temporarily computed by word length and number of number of operations * required to change one word into the other (Damerau-Levenshtein). It's subject to * change, cmp. https://github.com/otacke/udacity-machine-learning-engineer/blob/master/submissions/capstone_proposals/h5p_fuzzy_blanks.md * * @param {String} string1 - String #1. * @param {String} string2 - String #2. * @param {object} params - Parameters. * @return {boolean} True, if strings are considered to be similar. */ TextUtilities.areSimilar = function (string1, string2) { // Sanitization if (!string1 || typeof string1 !== 'string') { return; } if (!string2 || typeof string2 !== 'string') { return; } // Just temporariliy this unflexible. Will be configurable via params. var length = Math.min(string1.length, string2.length); var levenshtein = H5P.TextUtilities.computeLevenshteinDistance(string1, string2, true); if (levenshtein === 0) { return true; } if ((length > 9) && (levenshtein <= 2)) { return true; } if ((length > 3) && (levenshtein <= 1)) { return true; } return false; }; /** * Compute the (Damerau-)Levenshtein distance for two strings. * * The (Damerau-)Levenshtein distance that is returned is equivalent to the * number of operations that are necessary to transform one string into the * other. Consequently, lower numbers indicate higher similarity between the * two strings. * * While the Levenshtein distance counts deletions, insertions and mismatches, * the Damerau-Levenshtein distance also counts swapping two characters as * only one operation (instead of two mismatches), because this seems to * happen quite often. * * See http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance for details * * @public * @param {string} str1 - String no. 1. * @param {string} str2 - String no. 2. * @param {boolean} [countSwapping=false] - If true, swapping chars will count as operation. * @returns {number} Distance. */ TextUtilities.computeLevenshteinDistance = function(str1, str2, countSwapping) { // sanity checks if (typeof str1 !== 'string' || typeof str2 !== 'string') { return undefined; } if (countSwapping && typeof countSwapping !== 'boolean') { countSwapping = false; } // degenerate cases if (str1 === str2) { return 0; } if (str1.length === 0) { return str2.length; } if (str2.length === 0) { return str1.length; } // counter variables var i, j; // indicates characters that don't match var cost; // matrix for storing distances var distance = []; // initialization for (i = 0; i <= str1.length; i++) { distance[i] = [i]; } for (j = 0; j <= str2.length; j++) { distance[0][j] = j; } // computation for (i = 1; i <= str1.length; i++) { for (j = 1; j <= str2.length; j++) { cost = (str1[i-1] === str2[j-1]) ? 0 : 1; distance[i][j] = Math.min( distance[i-1][j] + 1, // deletion distance[i][j-1] + 1, // insertion distance[i-1][j-1] + cost // mismatch ); // in Damerau-Levenshtein distance, transpositions are operations if (countSwapping) { if (i > 1 && j > 1 && str1[i-1] === str2[j-2] && str1[i-2] === str2[j-1]) { distance[i][j] = Math.min(distance[i][j], distance[i-2][j-2] + cost); } } } } return distance[str1.length][str2.length]; }; /** * Compute the Jaro(-Winkler) distance for two strings. * * The Jaro(-Winkler) distance will return a value between 0 and 1 indicating * the similarity of two strings. The higher the value, the more similar the * strings are. * * See https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance for details * * It seems that a more generalized implementation of Winkler's modification * can improve the results. This might be implemented later. * http://disi.unitn.it/~p2p/RelatedWork/Matching/Hermans_bnaic-2012.pdf * * @public * @param {string} str1 - String no. 1. * @param {string} str2 - String no. 2. * @param {boolean} [favorSameStart=false] - If true, strings with same start get higher distance value. * @param {boolean} [longTolerance=false] - If true, Winkler's tolerance for long words will be used. * @returns {number} Distance. */ TextUtilities.computeJaroDistance = function(str1, str2, favorSameStart, longTolerance) { // sanity checks if (typeof str1 !== 'string' || typeof str2 !== 'string') { return undefined; } if (favorSameStart && typeof favorSameStart !== 'boolean') { favorSameStart = false; } if (longTolerance && typeof longTolerance !== 'boolean') { longTolerance = false; } // degenerate cases if (str1.length === 0 || str2.length === 0) { return 0; } if (str1 === str2) { return 1; } // counter variables var i, j, k; // number of matches between both strings var matches = 0; // number of transpositions between both strings var transpositions = 0; // The Jaro-Winkler distance var distance = 0; // length of common prefix up to 4 chars var l = 0; // scaling factor, should not exceed 0.25 (Winkler default = 0.1) var p = 0.1; // will be used often var str1Len = str1.length; var str2Len = str2.length; // determines the distance that still counts as a match var matchWindow = Math.floor(Math.max(str1Len, str2Len) / 2)- 1; // will store matches var str1Flags = new Array(str1Len); var str2Flags = new Array(str2Len); // count matches for (i = 0; i < str1Len; i++) { var start = (i >= matchWindow) ? i - matchWindow : 0; var end = (i + matchWindow <= (str2Len - 1)) ? (i + matchWindow) : (str2Len - 1); for (j = start; j <= end; j++) { if (str1Flags[i] !== true && str2Flags[j] !== true && str1[i] === str2[j]) { str1Flags[i] = str2Flags[j] = true; matches += 1; break; } } } if (matches === 0) { return 0; } // count transpositions k = 0; for (i = 0; i < str1Len; i++) { if (!str1Flags[i]) { continue; } while (!str2Flags[k]) { k += 1; } if (str1[i] !== str2[k]) { transpositions += 1; } k += 1; } transpositions = transpositions / 2; // compute Jaro distance distance = (matches/str1Len + matches/str2Len + (matches - transpositions) / matches) / 3; // modification used by Winkler if (favorSameStart) { if (distance > 0.7 && str1Len > 3 && str2Len > 3) { while (str1[l] === str2[l] && l < 4) { l += 1; } distance = distance + l * p * (1 - distance); // modification for long words if (longTolerance) { if (Math.max(str1Len, str2Len) > 4 && matches > l + 1 && 2 * matches >= Math.max(str1Len, str2Len) + l) { distance += ((1.0 - distance) * ((matches - l - 1) / (str1Len + str2Len - 2 * l + 2))); } } } } return distance; }; /** * Check whether a text contains a string, but fuzzy. * * This function is naive. It moves a window of needle's length (+2) * over the haystack's text and each move compares for similarity using * a given string metric. This will be slow for long texts!!! * * TODO: You might want to look into the bitap algorithm or experiment * with regexps * * @param {String} needle - String to look for. * @param {String} haystack - Text to look in. */ TextUtilities.fuzzyContains = function (needle, haystack) { return this.fuzzyFind(needle, haystack).contains; }; /** * Find the first position of a fuzzy string within a text * @param {String} needle - String to look for. * @param {String} haystack - Text to look in. */ TextUtilities.fuzzyIndexOf = function (needle, haystack) { return this.fuzzyFind(needle, haystack).indexOf; }; /** * Find the first fuzzy match of a string within a text * @param {String} needle - String to look for. * @param {String} haystack - Text to look in. */ TextUtilities.fuzzyMatch = function (needle, haystack) { return this.fuzzyFind(needle, haystack).match; }; /** * Find a fuzzy string with in a text. * TODO: This could be cleaned ... * @param {String} needle - String to look for. * @param {String} haystack - Text to look in. * @param {object} params - Parameters. */ TextUtilities.fuzzyFind = function (needle, haystack, params) { // Sanitization if (!needle || typeof needle !== 'string') { return false; } if (!haystack || typeof haystack !== 'string') { return false; } if (params === undefined || params.windowSize === undefined || typeof params.windowSize !== 'number') { params = {'windowSize': 3}; } var match; var found = haystack.split(' ').some(function(hay) { match = hay; return H5P.TextUtilities.areSimilar(needle, hay); }); if (found) { return {'contains' : found, 'match': match, 'index': haystack.indexOf(match)}; } // This is not used for single words but for phrases for (var i = 0; i < haystack.length - needle.length + 1; i++) { var hay = []; for (var j = 0; j < params.windowSize; j++) { hay[j] = haystack.substr(i, needle.length + j); } // Checking isIsolated will e.g. prevent finding beginnings of words for (var j = 0; j < hay.length; j++) { if (TextUtilities.isIsolated(hay[j], haystack) && TextUtilities.areSimilar(hay[j], needle)) { match = hay[j]; found = true; break; } } if (found) { break; } } if (!found) { match = undefined; } return {'contains' : found, 'match': match, 'index': haystack.indexOf(match)}; }; return TextUtilities; }(); ; /*global H5P*/ H5P.Blanks = (function ($, Question) { /** * @constant * @default */ var STATE_ONGOING = 'ongoing'; var STATE_CHECKING = 'checking'; var STATE_SHOWING_SOLUTION = 'showing-solution'; var STATE_FINISHED = 'finished'; const XAPI_ALTERNATIVE_EXTENSION = 'https://h5p.org/x-api/alternatives'; const XAPI_CASE_SENSITIVITY = 'https://h5p.org/x-api/case-sensitivity'; const XAPI_REPORTING_VERSION_EXTENSION = 'https://h5p.org/x-api/h5p-reporting-version'; /** * @typedef {Object} Params * Parameters/configuration object for Blanks * * @property {Object} Params.behaviour * @property {string} Params.behaviour.confirmRetryDialog * @property {string} Params.behaviour.confirmCheckDialog * * @property {Object} Params.confirmRetry * @property {string} Params.confirmRetry.header * @property {string} Params.confirmRetry.body * @property {string} Params.confirmRetry.cancelLabel * @property {string} Params.confirmRetry.confirmLabel * * @property {Object} Params.confirmCheck * @property {string} Params.confirmCheck.header * @property {string} Params.confirmCheck.body * @property {string} Params.confirmCheck.cancelLabel * @property {string} Params.confirmCheck.confirmLabel */ /** * Initialize module. * * @class H5P.Blanks * @extends H5P.Question * @param {Params} params * @param {number} id Content identification * @param {Object} contentData Task specific content data */ function Blanks(params, id, contentData) { var self = this; // Inheritance Question.call(self, 'blanks'); // IDs this.contentId = id; this.contentData = contentData; this.params = $.extend(true, {}, { text: "Fill in", questions: [ "<p>Oslo is the capital of *Norway*.</p>" ], overallFeedback: [], userAnswers: [], // TODO This isn't in semantics? showSolutions: "Show solution", tryAgain: "Try again", checkAnswer: "Check", changeAnswer: "Change answer", notFilledOut: "Please fill in all blanks to view solution", answerIsCorrect: "':ans' is correct", answerIsWrong: "':ans' is wrong", answeredCorrectly: "Answered correctly", answeredIncorrectly: "Answered incorrectly", solutionLabel: "Correct answer:", inputLabel: "Blank input @num of @total", inputHasTipLabel: "Tip available", tipLabel: "Tip", scoreBarLabel: 'You got :num out of :total points', behaviour: { enableRetry: true, enableSolutionsButton: true, enableCheckButton: true, caseSensitive: true, showSolutionsRequiresInput: true, autoCheck: false, separateLines: false }, a11yCheck: 'Check the answers. The responses will be marked as correct, incorrect, or unanswered.', a11yShowSolution: 'Show the solution. The task will be marked with its correct solution.', a11yRetry: 'Retry the task. Reset all responses and start the task over again.', a11yHeader: 'Checking mode', submitAnswer: 'Submit', }, params); // Delete empty questions for (var i = this.params.questions.length - 1; i >= 0; i--) { if (!this.params.questions[i]) { this.params.questions.splice(i, 1); } } // Previous state this.contentData = contentData; if (this.contentData !== undefined && this.contentData.previousState !== undefined) { this.previousState = this.contentData.previousState; } // Clozes this.clozes = []; // Keep track tabbing forward or backwards this.shiftPressed = false; H5P.$body.keydown(function (event) { if (event.keyCode === 16) { self.shiftPressed = true; } }).keyup(function (event) { if (event.keyCode === 16) { self.shiftPressed = false; } }); // Using instructions as label for our text groups this.labelId = 'h5p-blanks-instructions-' + Blanks.idCounter + '-' + H5P.createUUID(); this.content = self.createQuestions(); // Check for task media var media = self.params.media; if (media && media.type && media.type.library) { media = media.type; var type = media.library.split(' ')[0]; if (type === 'H5P.Image') { if (media.params.file) { // Register task image self.setImage(media.params.file.path, { disableImageZooming: self.params.media.disableImageZooming || false, alt: media.params.alt, title: media.params.title, expandImage: media.params.expandImage, minimizeImage: media.params.minimizeImage }); } } else if (type === 'H5P.Video') { if (media.params.sources) { // Register task video self.setVideo(media); } } else if (type === 'H5P.Audio') { if (media.params.files) { // Register task audio self.setAudio(media); } } } // Register task introduction text self.setIntroduction('<div id="' + this.labelId + '">' + self.params.text + '</div>'); // Register task content area self.setContent(self.content, { 'class': self.params.behaviour.separateLines ? 'h5p-separate-lines' : '' }); // ... and buttons self.registerButtons(); // Restore previous state self.setH5PUserState(); } // Inheritance Blanks.prototype = Object.create(Question.prototype); Blanks.prototype.constructor = Blanks; /** * Create all the buttons for the task */ Blanks.prototype.registerButtons = function () { var self = this; var $content = $('[data-content-id="' + self.contentId + '"].h5p-content'); var $containerParents = $content.parents('.h5p-container'); // select find container to attach dialogs to var $container; if ($containerParents.length !== 0) { // use parent highest up if any $container = $containerParents.last(); } else if ($content.length !== 0) { $container = $content; } else { $container = $(document.body); } if (!self.params.behaviour.autoCheck && this.params.behaviour.enableCheckButton) { // Check answer button self.addButton('check-answer', self.params.checkAnswer, function () { // Move focus to top of content self.a11yHeader.innerHTML = self.params.a11yHeader; self.a11yHeader.focus(); self.toggleButtonVisibility(STATE_CHECKING); self.markResults(); self.showEvaluation(); self.triggerAnswered(); }, true, { 'aria-label': self.params.a11yCheck, }, { confirmationDialog: { enable: self.params.behaviour.confirmCheckDialog, l10n: self.params.confirmCheck, instance: self, $parentElement: $container }, textIfSubmitting: self.params.submitAnswer, contentData: self.contentData, }); } // Show solution button self.addButton('show-solution', self.params.showSolutions, function () { self.showCorrectAnswers(false); }, self.params.behaviour.enableSolutionsButton, { 'aria-label': self.params.a11yShowSolution, }); // Try again button if (self.params.behaviour.enableRetry === true) { self.addButton('try-again', self.params.tryAgain, function () { self.a11yHeader.innerHTML = ''; self.resetTask(); self.$questions.filter(':first').find('input:first').focus(); }, true, { 'aria-label': self.params.a11yRetry, }, { confirmationDialog: { enable: self.params.behaviour.confirmRetryDialog, l10n: self.params.confirmRetry, instance: self, $parentElement: $container } }); } self.toggleButtonVisibility(STATE_ONGOING); }; /** * Find blanks in a string and run a handler on those blanks * * @param {string} question * Question text containing blanks enclosed in asterisks. * @param {function} handler * Replaces the blanks text with an input field. * @returns {string} * The question with blanks replaced by the given handler. */ Blanks.prototype.handleBlanks = function (question, handler) { // Go through the text and run handler on all asterisk var clozeEnd, clozeStart = question.indexOf('*'); var self = this; while (clozeStart !== -1 && clozeEnd !== -1) { clozeStart++; clozeEnd = question.indexOf('*', clozeStart); if (clozeEnd === -1) { continue; // No end } var clozeContent = question.substring(clozeStart, clozeEnd).replaceAll(/<\/?[a-z]*\d?>/ig, ''); var replacer = ''; if (clozeContent.length) { replacer = handler(self.parseSolution(clozeContent)); clozeEnd++; } else { clozeStart += 1; } question = question.slice(0, clozeStart - 1) + replacer + question.slice(clozeEnd); clozeEnd -= clozeEnd - clozeStart - replacer.length; // Find the next cloze clozeStart = question.indexOf('*', clozeEnd); } return question; }; /** * Create questitons html for DOM */ Blanks.prototype.createQuestions = function () { var self = this; var html = ''; var clozeNumber = 0; for (var i = 0; i < self.params.questions.length; i++) { var question = self.params.questions[i]; // Go through the question text and replace all the asterisks with input fields question = self.handleBlanks(question, function (solution) { // Create new cloze clozeNumber += 1; var defaultUserAnswer = (self.params.userAnswers.length > self.clozes.length ? self.params.userAnswers[self.clozes.length] : null); var cloze = new Blanks.Cloze(solution, self.params.behaviour, defaultUserAnswer, { answeredCorrectly: self.params.answeredCorrectly, answeredIncorrectly: self.params.answeredIncorrectly, solutionLabel: self.params.solutionLabel, inputLabel: self.params.inputLabel, inputHasTipLabel: self.params.inputHasTipLabel, tipLabel: self.params.tipLabel }); self.clozes.push(cloze); return cloze; }); html += '<div role="group" aria-labelledby="' + self.labelId + '">' + question + '</div>'; } self.hasClozes = clozeNumber > 0; this.$questions = $(html); self.a11yHeader = document.createElement('div'); self.a11yHeader.classList.add('hidden-but-read'); self.a11yHeader.tabIndex = -1; self.$questions[0].insertBefore(self.a11yHeader, this.$questions[0].childNodes[0] || null); // Set input fields. this.$questions.find('input').each(function (i) { /** * Observe resizing of input field, so that we can resize * the H5P to fit all content when the input field grows in size */ let resizeTimer; new ResizeObserver(function () { // To avoid triggering resize too often, we wait a second after the last // resize event has been received clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { self.trigger('resize'); }, 1000); }).observe(this); var afterCheck; if (self.params.behaviour.autoCheck) { afterCheck = function () { var answer = $("<div>").text(this.getUserAnswer()).html(); self.read((this.correct() ? self.params.answerIsCorrect : self.params.answerIsWrong).replace(':ans', answer)); if (self.done || self.allBlanksFilledOut()) { // All answers has been given. Show solutions button. self.toggleButtonVisibility(STATE_CHECKING); self.showEvaluation(); self.triggerAnswered(); self.done = true; } }; } self.clozes[i].setInput($(this), afterCheck, function () { self.toggleButtonVisibility(STATE_ONGOING); if (!self.params.behaviour.autoCheck) { self.hideEvaluation(); } }, i, self.clozes.length); }).keydown(function (event) { var $this = $(this); // Adjust width of text input field to match value self.autoGrowTextField($this); var $inputs, isLastInput; var enterPressed = (event.keyCode === 13); var tabPressedAutoCheck = (event.keyCode === 9 && self.params.behaviour.autoCheck); if (enterPressed || tabPressedAutoCheck) { // Figure out which inputs are left to answer $inputs = self.$questions.find('.h5p-input-wrapper:not(.h5p-correct) .h5p-text-input'); // Figure out if this is the last input isLastInput = $this.is($inputs[$inputs.length - 1]); } if ((tabPressedAutoCheck && isLastInput && !self.shiftPressed) || (enterPressed && isLastInput)) { // Focus first button on next tick setTimeout(function () { self.focusButton(); }, 10); } if (enterPressed) { if (isLastInput) { // Check answers $this.trigger('blur'); } else { // Find next input to focus $inputs.eq($inputs.index($this) + 1).focus(); } return false; // Prevent form submission on enter key } }).on('change', function () { self.answered = true; self.triggerXAPI('interacted'); }); self.on('resize', function () { self.resetGrowTextField(); }); return this.$questions; }; /** * */ Blanks.prototype.autoGrowTextField = function ($input) { // Do not set text field size when separate lines is enabled if (this.params.behaviour.separateLines) { return; } var self = this; var fontSize = parseInt($input.css('font-size'), 10); var minEm = 3; var minPx = fontSize * minEm; var rightPadEm = 3.25; var rightPadPx = fontSize * rightPadEm; var static_min_pad = 0.5 * fontSize; setTimeout(function () { var tmp = $('<div>', { 'text': $input.val() }); tmp.css({ 'position': 'absolute', 'white-space': 'nowrap', 'font-size': $input.css('font-size'), 'font-family': $input.css('font-family'), 'padding': $input.css('padding'), 'width': 'initial' }); $input.parent().append(tmp); var width = tmp.width(); var parentWidth = self.$questions.width(); tmp.remove(); if (width <= minPx) { // Apply min width $input.width(minPx + static_min_pad); } else if (width + rightPadPx >= parentWidth) { // Apply max width of parent $input.width(parentWidth - rightPadPx); } else { // Apply width that wraps input $input.width(width + static_min_pad); } }, 1); }; /** * Resize all text field growth to current size. */ Blanks.prototype.resetGrowTextField = function () { var self = this; this.$questions.find('input').each(function () { self.autoGrowTextField($(this)); }); }; /** * Toggle buttons dependent of state. * * Using CSS-rules to conditionally show/hide using the data-attribute [data-state] */ Blanks.prototype.toggleButtonVisibility = function (state) { // The show solutions button is hidden if all answers are correct var allCorrect = (this.getScore() === this.getMaxScore()); if (this.params.behaviour.autoCheck && allCorrect) { // We are viewing the solutions state = STATE_FINISHED; } if (this.params.behaviour.enableSolutionsButton) { if (state === STATE_CHECKING && !allCorrect) { this.showButton('show-solution'); } else { this.hideButton('show-solution'); } } if (this.params.behaviour.enableRetry) { if ((state === STATE_CHECKING && !allCorrect) || state === STATE_SHOWING_SOLUTION) { this.showButton('try-again'); } else { this.hideButton('try-again'); } } if (state === STATE_ONGOING) { this.showButton('check-answer'); } else { this.hideButton('check-answer'); } this.trigger('resize'); }; /** * Check if solution is allowed. Warn user if not */ Blanks.prototype.allowSolution = function () { if (this.params.behaviour.showSolutionsRequiresInput === true) { if (!this.allBlanksFilledOut()) { this.updateFeedbackContent(this.params.notFilledOut); this.read(this.params.notFilledOut); return false; } } return true; }; /** * Check if all blanks are filled out * * @method allBlanksFilledOut * @return {boolean} Returns true if all blanks are filled out. */ Blanks.prototype.allBlanksFilledOut = function () { return !this.clozes.some(function (cloze) { return !cloze.filledOut(); }); }; /** * Mark which answers are correct and which are wrong and disable fields if retry is off. */ Blanks.prototype.markResults = function () { var self = this; for (var i = 0; i < self.clozes.length; i++) { self.clozes[i].checkAnswer(); if (!self.params.behaviour.enableRetry) { self.clozes[i].disableInput(); } } this.trigger('resize'); }; /** * Removed marked results */ Blanks.prototype.removeMarkedResults = function () { this.$questions.find('.h5p-input-wrapper').removeClass('h5p-correct h5p-wrong'); this.$questions.find('.h5p-input-wrapper > input').attr('disabled', false); this.trigger('resize'); }; /** * Displays the correct answers * @param {boolean} [alwaysShowSolution] * Will always show solution if true */ Blanks.prototype.showCorrectAnswers = function (alwaysShowSolution) { if (!alwaysShowSolution && !this.allowSolution()) { return; } this.toggleButtonVisibility(STATE_SHOWING_SOLUTION); this.hideSolutions(); for (var i = 0; i < this.clozes.length; i++) { this.clozes[i].showSolution(); } this.trigger('resize'); }; /** * Toggle input allowed for all input fields * * @method function * @param {boolean} enabled True if fields should allow input, otherwise false */ Blanks.prototype.toggleAllInputs = function (enabled) { for (var i = 0; i < this.clozes.length; i++) { this.clozes[i].toggleInput(enabled); } }; /** * Display the correct solution for the input boxes. * * This is invoked from CP and QS - be carefull! */ Blanks.prototype.showSolutions = function () { this.params.behaviour.enableSolutionsButton = true; this.toggleButtonVisibility(STATE_FINISHED); this.markResults(); this.showEvaluation(); this.showCorrectAnswers(true); this.toggleAllInputs(false); //Hides all buttons in "show solution" mode. this.hideButtons(); }; /** * Resets the complete task. * Used in contracts. * @public */ Blanks.prototype.resetTask = function () { this.answered = false; this.hideEvaluation(); this.hideSolutions(); this.clearAnswers(); this.removeMarkedResults(); this.toggleButtonVisibility(STATE_ONGOING); this.resetGrowTextField(); this.toggleAllInputs(true); this.done = false; }; /** * Hides all buttons. * @public */ Blanks.prototype.hideButtons = function () { this.toggleButtonVisibility(STATE_FINISHED); }; /** * Trigger xAPI answered event */ Blanks.prototype.triggerAnswered = function () { this.answered = true; var xAPIEvent = this.createXAPIEventTemplate('answered'); this.addQuestionToXAPI(xAPIEvent); this.addResponseToXAPI(xAPIEvent); this.trigger(xAPIEvent); }; /** * Get xAPI data. * Contract used by report rendering engine. * * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6} */ Blanks.prototype.getXAPIData = function () { var xAPIEvent = this.createXAPIEventTemplate('answered'); this.addQuestionToXAPI(xAPIEvent); this.addResponseToXAPI(xAPIEvent); return { statement: xAPIEvent.data.statement }; }; /** * Generate xAPI object definition used in xAPI statements. * @return {Object} */ Blanks.prototype.getxAPIDefinition = function () { var definition = {}; // The below replaceAll makes sure we don't get any unwanted XAPI_PLACEHOLDERs in the description definition.description = { 'en-US': this.params.text.replaceAll(/_{10,}/gi, '_________') }; definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction'; definition.interactionType = 'fill-in'; const clozeSolutions = []; let crp = ''; // xAPI forces us to create solution patterns for all possible solution combinations for (var i = 0; i < this.params.questions.length; i++) { // The below replaceAll makes sure we don't get any unwanted XAPI_PLACEHOLDERs in the questions let question = this.params.questions[i].replaceAll(/_{10,}/gi, '_________'); question = this.handleBlanks(question, function (solution) { // Collect all solution combinations for the H5P Alternative extension clozeSolutions.push(solution.solutions); // Create a basic response pattern out of the first alternative for each blanks field crp += (!crp ? '' : '[,]') + solution.solutions[0]; // We replace the solutions in the question with a "blank" return '__________'; }); definition.description['en-US'] += question; } // Set the basic response pattern (not supporting multiple alternatives for blanks) definition.correctResponsesPattern = [ '{case_matters=' + this.params.behaviour.caseSensitive + '}' + crp, ]; // Add the H5P Alternative extension which provides all the combinations of different answers // Reporting software will need to support this extension for alternatives to work. definition.extensions = definition.extensions || {}; definition.extensions[XAPI_CASE_SENSITIVITY] = this.params.behaviour.caseSensitive; definition.extensions[XAPI_ALTERNATIVE_EXTENSION] = clozeSolutions; return definition; }; /** * Add the question itselt to the definition part of an xAPIEvent */ Blanks.prototype.addQuestionToXAPI = function (xAPIEvent) { var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']); $.extend(true, definition, this.getxAPIDefinition()); // Set reporting module version if alternative extension is used if (this.hasAlternatives) { const context = xAPIEvent.getVerifiedStatementValue(['context']); context.extensions = context.extensions || {}; context.extensions[XAPI_REPORTING_VERSION_EXTENSION] = '1.1.0'; } }; /** * Parse the solution text (text between the asterisks) * * @param {string} solutionText * @returns {object} with the following properties * - tip: the tip text for this solution, undefined if no tip * - solutions: array of solution words */ Blanks.prototype.parseSolution = function (solutionText) { var tip, solution; var tipStart = solutionText.indexOf(':'); if (tipStart !== -1) { // Found tip, now extract tip = solutionText.slice(tipStart + 1); solution = solutionText.slice(0, tipStart); } else { solution = solutionText; } // Split up alternatives var solutions = solution.split('/'); this.hasAlternatives = this.hasAlternatives || solutions.length > 1; // Trim solutions for (var i = 0; i < solutions.length; i++) { solutions[i] = H5P.trim(solutions[i]); //decodes html entities var elem = document.createElement('textarea'); elem.innerHTML = solutions[i]; solutions[i] = elem.value; } return { tip: tip, solutions: solutions }; }; /** * Add the response part to an xAPI event * * @param {H5P.XAPIEvent} xAPIEvent * The xAPI event we will add a response to */ Blanks.prototype.addResponseToXAPI = function (xAPIEvent) { xAPIEvent.setScoredResult(this.getScore(), this.getMaxScore(), this); xAPIEvent.data.statement.result.response = this.getxAPIResponse(); }; /** * Generate xAPI user response, used in xAPI statements. * @return {string} User answers separated by the "[,]" pattern */ Blanks.prototype.getxAPIResponse = function () { var usersAnswers = this.getCurrentState(); return usersAnswers.join('[,]'); }; /** * Show evaluation widget, i.e: 'You got x of y blanks correct' */ Blanks.prototype.showEvaluation = function () { var maxScore = this.getMaxScore(); var score = this.getScore(); var scoreText = H5P.Question.determineOverallFeedback(this.params.overallFeedback, score / maxScore).replace('@score', score).replace('@total', maxScore); this.setFeedback(scoreText, score, maxScore, this.params.scoreBarLabel); if (score === maxScore) { this.toggleButtonVisibility(STATE_FINISHED); } }; /** * Hide the evaluation widget */ Blanks.prototype.hideEvaluation = function () { // Clear evaluation section. this.removeFeedback(); }; /** * Hide solutions. (/try again) */ Blanks.prototype.hideSolutions = function () { // Clean solution from quiz this.$questions.find('.h5p-correct-answer').remove(); }; /** * Get maximum number of correct answers. * * @returns {Number} Max points */ Blanks.prototype.getMaxScore = function () { var self = this; return self.clozes.length; }; /** * Count the number of correct answers. * * @returns {Number} Points */ Blanks.prototype.getScore = function () { var self = this; var correct = 0; for (var i = 0; i < self.clozes.length; i++) { if (self.clozes[i].correct()) { correct++; } self.params.userAnswers[i] = self.clozes[i].getUserAnswer(); } return correct; }; Blanks.prototype.getTitle = function () { return H5P.createTitle((this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title : 'Fill In'); }; /** * Clear the user's answers */ Blanks.prototype.clearAnswers = function () { this.clozes.forEach(function (cloze) { cloze.setUserInput(''); cloze.resetAriaLabel(); }); }; /** * Checks if all has been answered. * * @returns {Boolean} */ Blanks.prototype.getAnswerGiven = function () { return this.answered || !this.hasClozes; }; /** * Helps set focus the given input field. * @param {jQuery} $input */ Blanks.setFocus = function ($input) { setTimeout(function () { $input.focus(); }, 1); }; /** * Returns an object containing content of each cloze * * @returns {object} object containing content for each cloze */ Blanks.prototype.getCurrentState = function () { var clozesContent = []; // Get user input for every cloze this.clozes.forEach(function (cloze) { clozesContent.push(cloze.getUserAnswer()); }); return clozesContent; }; /** * Sets answers to current user state */ Blanks.prototype.setH5PUserState = function () { var self = this; var isValidState = (this.previousState !== undefined && this.previousState.length && this.previousState.length === this.clozes.length); // Check that stored user state is valid if (!isValidState) { return; } // Set input from user state var hasAllClozesFilled = true; this.previousState.forEach(function (clozeContent, ccIndex) { // Register that an answer has been given if (clozeContent.length) { self.answered = true; } var cloze = self.clozes[ccIndex]; cloze.setUserInput(clozeContent); // Handle instant feedback if (self.params.behaviour.autoCheck) { if (cloze.filledOut()) { cloze.checkAnswer(); } else { hasAllClozesFilled = false; } } }); if (self.params.behaviour.autoCheck && hasAllClozesFilled) { self.showEvaluation(); self.toggleButtonVisibility(STATE_CHECKING); } }; /** * Disables any active input. Useful for freezing the task and dis-allowing * modification of wrong answers. */ Blanks.prototype.disableInput = function () { this.$questions.find('input').attr('disabled', true); }; Blanks.idCounter = 0; return Blanks; })(H5P.jQuery, H5P.Question); /** * Static utility method for parsing H5P.Blanks qestion into a format useful * for creating reports. * * Example question: 'H5P content may be edited using a *browser/web-browser:something you use every day*.' * * Produces the following result: * [ * { * type: 'text', * content: 'H5P content may be edited using a ' * }, * { * type: 'answer', * correct: ['browser', 'web-browser'] * }, * { * type: 'text', * content: '.' * } * ] * * @param {string} question */ H5P.Blanks.parseText = function (question) { var blank = new H5P.Blanks({ question: question }); /** * Parses a text into an array where words starting and ending * with an asterisk are separated from other text. * e.g ["this", "*is*", " an ", "*example*"] * * @param {string} text * * @return {string[]} */ function tokenizeQuestionText(text) { return text.split(/(\*.*?\*)/).filter(function (str) { return str.length > 0; } ); } function startsAndEndsWithAnAsterisk(str) { return str.substr(0,1) === '*' && str.substr(-1) === '*'; } function replaceHtmlTags(str, value) { return str.replace(/<[^>]*>/g, value); } return tokenizeQuestionText(replaceHtmlTags(question, '')).map(function (part) { return startsAndEndsWithAnAsterisk(part) ? ({ type: 'answer', correct: blank.parseSolution(part.slice(1, -1)).solutions }) : ({ type: 'text', content: part }); }); }; ; (function ($, Blanks) { /** * Simple private class for keeping track of clozes. * * @class H5P.Blanks.Cloze * @param {string} answer * @param {Object} behaviour Behavioral settings for the task from semantics * @param {boolean} behaviour.acceptSpellingErrors - If true, answers will also count correct if they contain small spelling errors. * @param {string} defaultUserAnswer * @param {Object} l10n Localized texts * @param {string} l10n.solutionLabel Assistive technology label for cloze solution * @param {string} l10n.inputLabel Assistive technology label for cloze input * @param {string} l10n.inputHasTipLabel Assistive technology label for input with tip * @param {string} l10n.tipLabel Label for tip icon */ Blanks.Cloze = function (solution, behaviour, defaultUserAnswer, l10n) { var self = this; var $input, $wrapper; var answers = solution.solutions; var answer = answers.join('/'); var tip = solution.tip; var checkedAnswer = null; var inputLabel = l10n.inputLabel; if (behaviour.caseSensitive !== true) { // Convert possible solutions into lowercase for (var i = 0; i < answers.length; i++) { answers[i] = answers[i].toLowerCase(); } } /** * Check if the answer is correct. * * @private * @param {string} answered */ var correct = function (answered) { if (behaviour.caseSensitive !== true) { answered = answered.toLowerCase(); } for (var i = 0; i < answers.length; i++) { // Damerau-Levenshtein comparison if (behaviour.acceptSpellingErrors === true) { var levenshtein = H5P.TextUtilities.computeLevenshteinDistance(answered, H5P.trim(answers[i]), true); /* * The correctness is temporarily computed by word length and number of number of operations * required to change one word into the other (Damerau-Levenshtein). It's subject to * change, cmp. https://github.com/otacke/udacity-machine-learning-engineer/blob/master/submissions/capstone_proposals/h5p_fuzzy_blanks.md */ if ((answers[i].length > 9) && (levenshtein <= 2)) { return true; } else if ((answers[i].length > 3) && (levenshtein <= 1)) { return true; } } // regular comparison if (answered === H5P.trim(answers[i])) { return true; } } return false; }; /** * Check if filled out. * * @param {boolean} */ this.filledOut = function () { var answered = this.getUserAnswer(); // Blank can be correct and is interpreted as filled out. return (answered !== '' || correct(answered)); }; /** * Check the cloze and mark it as wrong or correct. */ this.checkAnswer = function () { checkedAnswer = this.getUserAnswer(); var isCorrect = correct(checkedAnswer); if (isCorrect) { $wrapper.addClass('h5p-correct'); $input.attr('disabled', true) .attr('aria-label', inputLabel + '. ' + l10n.answeredCorrectly); } else { $wrapper.addClass('h5p-wrong'); $input.attr('aria-label', inputLabel + '. ' + l10n.answeredIncorrectly); } }; /** * Disables input. * @method disableInput */ this.disableInput = function () { this.toggleInput(false); }; /** * Enables input. * @method enableInput */ this.enableInput = function () { this.toggleInput(true); }; /** * Toggles input enable/disable * @method toggleInput * @param {boolean} enabled True if input should be enabled, otherwise false */ this.toggleInput = function (enabled) { $input.attr('disabled', !enabled); }; /** * Show the correct solution. */ this.showSolution = function () { if (correct(this.getUserAnswer())) { return; // Only for the wrong ones } $('<span>', { 'aria-hidden': true, 'class': 'h5p-correct-answer', text: H5P.trim(answer.replace(/\s*\/\s*/g, '/')), insertAfter: $wrapper }); $input.attr('disabled', true); var ariaLabel = inputLabel + '. ' + l10n.solutionLabel + ' ' + answer + '. ' + l10n.answeredIncorrectly; $input.attr('aria-label', ariaLabel); }; /** * @returns {boolean} */ this.correct = function () { return correct(this.getUserAnswer()); }; /** * Set input element. * * @param {H5P.jQuery} $element * @param {function} afterCheck * @param {function} afterFocus * @param {number} clozeIndex Index of cloze * @param {number} totalCloze Total amount of clozes in blanks */ this.setInput = function ($element, afterCheck, afterFocus, clozeIndex, totalCloze) { $input = $element; $wrapper = $element.parent(); inputLabel = inputLabel.replace('@num', (clozeIndex + 1)) .replace('@total', totalCloze); // Add tip if tip is set if(tip !== undefined && tip.trim().length > 0) { $wrapper.addClass('has-tip') .append(H5P.JoubelUI.createTip(tip, { tipLabel: l10n.tipLabel })); inputLabel += '. ' + l10n.inputHasTipLabel; } $input.attr('aria-label', inputLabel); if (afterCheck !== undefined) { $input.blur(function () { if (self.filledOut()) { // Check answers if (!behaviour.enableRetry) { self.disableInput(); } self.checkAnswer(); afterCheck.apply(self); } }); } $input.keyup(function () { if (checkedAnswer !== null && checkedAnswer !== self.getUserAnswer()) { // The Answer has changed since last check checkedAnswer = null; $wrapper.removeClass('h5p-wrong'); $input.attr('aria-label', inputLabel); if (afterFocus !== undefined) { afterFocus(); } } }); }; /** * @returns {string} Cloze html */ this.toString = function () { var extra = defaultUserAnswer ? ' value="' + defaultUserAnswer + '"' : ''; var result = '<span class="h5p-input-wrapper"><input type="text" class="h5p-text-input" autocomplete="off" autocapitalize="off" spellcheck="false"' + extra + '></span>'; self.length = result.length; return result; }; /** * @returns {string} Trimmed answer */ this.getUserAnswer = function () { const trimmedAnswer = H5P.trim($input.val().replace(/\ /g, ' ')); // Set trimmed answer $input.val(trimmedAnswer); if (behaviour.formulaEditor) { // If fomula editor is enabled set trimmed text $input.parent().find('.wiris-h5p-input').html(trimmedAnswer); } return trimmedAnswer; }; /** * @param {string} text New input text */ this.setUserInput = function (text) { $input.val(text); }; /** * Resets aria label of input field */ this.resetAriaLabel = function () { $input.attr('aria-label', inputLabel); }; }; })(H5P.jQuery, H5P.Blanks); ; // Will render a Question with multiple choices for answers. // Options format: // { // title: "Optional title for question box", // question: "Question text", // answers: [{text: "Answer text", correct: false}, ...], // singleAnswer: true, // or false, will change rendered output slightly. // singlePoint: true, // True if question give a single point score only // // if all are correct, false to give 1 point per // // correct answer. (Only for singleAnswer=false) // randomAnswers: false // Whether to randomize the order of answers. // } // // Events provided: // - h5pQuestionAnswered: Triggered when a question has been answered. var H5P = H5P || {}; /** * @typedef {Object} Options * Options for multiple choice * * @property {Object} behaviour * @property {boolean} behaviour.confirmCheckDialog * @property {boolean} behaviour.confirmRetryDialog * * @property {Object} UI * @property {string} UI.tipsLabel * * @property {Object} [confirmRetry] * @property {string} [confirmRetry.header] * @property {string} [confirmRetry.body] * @property {string} [confirmRetry.cancelLabel] * @property {string} [confirmRetry.confirmLabel] * * @property {Object} [confirmCheck] * @property {string} [confirmCheck.header] * @property {string} [confirmCheck.body] * @property {string} [confirmCheck.cancelLabel] * @property {string} [confirmCheck.confirmLabel] */ /** * Module for creating a multiple choice question * * @param {Options} options * @param {number} contentId * @param {Object} contentData * @returns {H5P.MultiChoice} * @constructor */ H5P.MultiChoice = function (options, contentId, contentData) { if (!(this instanceof H5P.MultiChoice)) return new H5P.MultiChoice(options, contentId, contentData); var self = this; this.contentId = contentId; this.contentData = contentData; H5P.Question.call(self, 'multichoice'); var $ = H5P.jQuery; var defaults = { image: null, question: "No question text provided", answers: [ { tipsAndFeedback: { tip: '', chosenFeedback: '', notChosenFeedback: '' }, text: "Answer 1", correct: true } ], overallFeedback: [], weight: 1, userAnswers: [], UI: { checkAnswerButton: 'Check', submitAnswerButton: 'Submit', showSolutionButton: 'Show solution', tryAgainButton: 'Try again', scoreBarLabel: 'You got :num out of :total points', tipAvailable: "Tip available", feedbackAvailable: "Feedback available", readFeedback: 'Read feedback', shouldCheck: "Should have been checked", shouldNotCheck: "Should not have been checked", noInput: 'Input is required before viewing the solution', a11yCheck: 'Check the answers. The responses will be marked as correct, incorrect, or unanswered.', a11yShowSolution: 'Show the solution. The task will be marked with its correct solution.', a11yRetry: 'Retry the task. Reset all responses and start the task over again.', }, behaviour: { enableRetry: true, enableSolutionsButton: true, enableCheckButton: true, type: 'auto', singlePoint: true, randomAnswers: false, showSolutionsRequiresInput: true, autoCheck: false, passPercentage: 100, showScorePoints: true } }; var params = $.extend(true, defaults, options); // Keep track of number of correct choices var numCorrect = 0; // Loop through choices for (var i = 0; i < params.answers.length; i++) { var answer = params.answers[i]; // Make sure tips and feedback exists answer.tipsAndFeedback = answer.tipsAndFeedback || {}; if (params.answers[i].correct) { // Update number of correct choices numCorrect++; } } // Determine if no choices is the correct var blankIsCorrect = (numCorrect === 0); // Determine task type if (params.behaviour.type === 'auto') { // Use single choice if only one choice is correct params.behaviour.singleAnswer = (numCorrect === 1); } else { params.behaviour.singleAnswer = (params.behaviour.type === 'single'); } var getCheckboxOrRadioIcon = function (radio, selected) { var icon; if (radio) { icon = selected ? '' : ''; } else { icon = selected ? '' : ''; } return icon; }; // Initialize buttons and elements. var $myDom; var $feedbackDialog; /** * Remove all feedback dialogs */ var removeFeedbackDialog = function () { // Remove the open feedback dialogs. $myDom.unbind('click', removeFeedbackDialog); $myDom.find('.h5p-feedback-button, .h5p-feedback-dialog').remove(); $myDom.find('.h5p-has-feedback').removeClass('h5p-has-feedback'); if ($feedbackDialog) { $feedbackDialog.remove(); } }; var score = 0; var solutionsVisible = false; /** * Add feedback to element * @param {jQuery} $element Element that feedback will be added to * @param {string} feedback Feedback string */ var addFeedback = function ($element, feedback) { $feedbackDialog = $('' + '<div class="h5p-feedback-dialog">' + '<div class="h5p-feedback-inner">' + '<div class="h5p-feedback-text">' + feedback + '</div>' + '</div>' + '</div>'); //make sure feedback is only added once if (!$element.find($('.h5p-feedback-dialog')).length) { $feedbackDialog.appendTo($element.addClass('h5p-has-feedback')); } }; /** * Register the different parts of the task with the H5P.Question structure. */ self.registerDomElements = function () { var media = params.media; if (media && media.type && media.type.library) { media = media.type; var type = media.library.split(' ')[0]; if (type === 'H5P.Image') { if (media.params.file) { // Register task image self.setImage(media.params.file.path, { disableImageZooming: params.media.disableImageZooming || false, alt: media.params.alt, title: media.params.title, expandImage: media.params.expandImage, minimizeImage: media.params.minimizeImage }); } } else if (type === 'H5P.Video') { if (media.params.sources) { // Register task video self.setVideo(media); } } else if (type === 'H5P.Audio') { if (media.params.files) { // Register task audio self.setAudio(media); } } } // Determine if we're using checkboxes or radio buttons for (var i = 0; i < params.answers.length; i++) { params.answers[i].checkboxOrRadioIcon = getCheckboxOrRadioIcon(params.behaviour.singleAnswer, params.userAnswers.indexOf(i) > -1); } // Register Introduction self.setIntroduction('<div id="' + params.labelId + '">' + params.question + '</div>'); // Register task content area $myDom = $('<ul>', { 'class': 'h5p-answers', role: params.role, 'aria-labelledby': params.labelId }); for (let i = 0; i < params.answers.length; i++) { const answer = params.answers[i]; answer.text = answer.text ?? '<div></div>'; $('<li>', { 'class': 'h5p-answer', role: answer.role, tabindex: answer.tabindex, 'aria-checked': answer.checked, 'data-id': i, html: '<div class="h5p-alternative-container"><span class="h5p-alternative-inner">' + answer.text + '</span></div>', appendTo: $myDom }); } self.setContent($myDom, { 'class': params.behaviour.singleAnswer ? 'h5p-radio' : 'h5p-check' }); // Create tips: var $answers = $('.h5p-answer', $myDom).each(function (i) { var tip = params.answers[i].tipsAndFeedback.tip; if (tip === undefined) { return; // No tip } tip = tip.trim(); var tipContent = tip .replace(/ /g, '') .replace(/<p>/g, '') .replace(/<\/p>/g, '') .trim(); if (!tipContent.length) { return; // Empty tip } else { $(this).addClass('h5p-has-tip'); } // Add tip var $wrap = $('<div/>', { 'class': 'h5p-multichoice-tipwrap', 'aria-label': params.UI.tipAvailable + '.' }); var $multichoiceTip = $('<div>', { 'role': 'button', 'tabindex': 0, 'title': params.UI.tipsLabel, 'aria-label': params.UI.tipsLabel, 'aria-expanded': false, 'class': 'multichoice-tip', appendTo: $wrap }); var tipIconHtml = '<span class="joubel-icon-tip-normal">' + '<span class="h5p-icon-shadow"></span>' + '<span class="h5p-icon-speech-bubble"></span>' + '<span class="h5p-icon-info"></span>' + '</span>'; $multichoiceTip.append(tipIconHtml); $multichoiceTip.click(function () { var $tipContainer = $multichoiceTip.parents('.h5p-answer'); var openFeedback = !$tipContainer.children('.h5p-feedback-dialog').is($feedbackDialog); removeFeedbackDialog(); // Do not open feedback if it was open if (openFeedback) { $multichoiceTip.attr('aria-expanded', true); // Add tip dialog addFeedback($tipContainer, tip); $feedbackDialog.addClass('h5p-has-tip'); // Tip for readspeaker self.read(tip); } else { $multichoiceTip.attr('aria-expanded', false); } self.trigger('resize'); // Remove tip dialog on dom click setTimeout(function () { $myDom.click(removeFeedbackDialog); }, 100); // Do not propagate return false; }).keydown(function (e) { if (e.which === 32) { $(this).click(); return false; } }); $('.h5p-alternative-container', this).append($wrap); }); // Set event listeners. var toggleCheck = function ($ans) { if ($ans.attr('aria-disabled') === 'true') { return; } self.answered = true; var num = parseInt($ans.data('id')); if (params.behaviour.singleAnswer) { // Store answer params.userAnswers = [num]; // Calculate score score = (params.answers[num].correct ? 1 : 0); // De-select previous answer $answers.not($ans).removeClass('h5p-selected').attr('tabindex', '-1').attr('aria-checked', 'false'); // Select new answer $ans.addClass('h5p-selected').attr('tabindex', '0').attr('aria-checked', 'true'); } else { if ($ans.attr('aria-checked') === 'true') { const pos = params.userAnswers.indexOf(num); if (pos !== -1) { params.userAnswers.splice(pos, 1); } // Do not allow un-checking when retry disabled and auto check if (params.behaviour.autoCheck && !params.behaviour.enableRetry) { return; } // Remove check $ans.removeClass('h5p-selected').attr('aria-checked', 'false'); } else { params.userAnswers.push(num); $ans.addClass('h5p-selected').attr('aria-checked', 'true'); } // Calculate score calcScore(); } self.triggerXAPI('interacted'); hideSolution($ans); if (params.userAnswers.length) { self.showButton('check-answer'); self.hideButton('try-again'); self.hideButton('show-solution'); if (params.behaviour.autoCheck) { if (params.behaviour.singleAnswer) { // Only a single answer allowed checkAnswer(); } else { // Show feedback for selected alternatives self.showCheckSolution(true); // Always finish task if it was completed successfully if (score === self.getMaxScore()) { checkAnswer(); } } } } }; $answers.click(function () { toggleCheck($(this)); }).keydown(function (e) { if (e.keyCode === 32) { // Space bar // Select current item toggleCheck($(this)); return false; } if (params.behaviour.singleAnswer) { switch (e.keyCode) { case 38: // Up case 37: { // Left // Try to select previous item var $prev = $(this).prev(); if ($prev.length) { toggleCheck($prev.focus()); } return false; } case 40: // Down case 39: { // Right // Try to select next item var $next = $(this).next(); if ($next.length) { toggleCheck($next.focus()); } return false; } } } }); if (params.behaviour.singleAnswer) { // Special focus handler for radio buttons $answers.focus(function () { if ($(this).attr('aria-disabled') !== 'true') { $answers.not(this).attr('tabindex', '-1'); } }).blur(function () { if (!$answers.filter('.h5p-selected').length) { $answers.first().add($answers.last()).attr('tabindex', '0'); } }); } // Adds check and retry button addButtons(); if (!params.behaviour.singleAnswer) { calcScore(); } else { if (params.userAnswers.length && params.answers[params.userAnswers[0]].correct) { score = 1; } else { score = 0; } } // Has answered through auto-check in a previous session if (hasCheckedAnswer && params.behaviour.autoCheck) { // Check answers if answer has been given or max score reached if (params.behaviour.singleAnswer || score === self.getMaxScore()) { checkAnswer(); } else { // Show feedback for checked checkboxes self.showCheckSolution(true); } } }; this.showAllSolutions = function () { if (solutionsVisible) { return; } solutionsVisible = true; $myDom.find('.h5p-answer').each(function (i, e) { var $e = $(e); var a = params.answers[i]; const className = 'h5p-solution-icon-' + (params.behaviour.singleAnswer ? 'radio' : 'checkbox'); if (a.correct) { $e.addClass('h5p-should').append($('<span/>', { 'class': className, html: params.UI.shouldCheck + '.' })); } else { $e.addClass('h5p-should-not').append($('<span/>', { 'class': className, html: params.UI.shouldNotCheck + '.' })); } }).find('.h5p-question-plus-one, .h5p-question-minus-one').remove(); // Make sure input is disabled in solution mode disableInput(); // Move focus back to the first alternative so that the user becomes // aware that the solution is being shown. $myDom.find('.h5p-answer:first-child').focus(); //Hide buttons and retry depending on settings. self.hideButton('check-answer'); self.hideButton('show-solution'); if (params.behaviour.enableRetry) { self.showButton('try-again'); } self.trigger('resize'); }; /** * Used in contracts. * Shows the solution for the task and hides all buttons. */ this.showSolutions = function () { removeFeedbackDialog(); self.showCheckSolution(); self.showAllSolutions(); disableInput(); self.hideButton('try-again'); }; /** * Hide solution for the given answer(s) * * @private * @param {H5P.jQuery} $answer */ var hideSolution = function ($answer) { $answer .removeClass('h5p-correct') .removeClass('h5p-wrong') .removeClass('h5p-should') .removeClass('h5p-should-not') .removeClass('h5p-has-feedback') .find('.h5p-question-plus-one, ' + '.h5p-question-minus-one, ' + '.h5p-answer-icon, ' + '.h5p-solution-icon-radio, ' + '.h5p-solution-icon-checkbox, ' + '.h5p-feedback-dialog') .remove(); }; /** * */ this.hideSolutions = function () { solutionsVisible = false; hideSolution($('.h5p-answer', $myDom)); this.removeFeedback(); // Reset feedback self.trigger('resize'); }; /** * Resets the whole task. * Used in contracts with integrated content. * @private * @param {boolean} moveFocus True to move the focus to first option * This prevents loss of focus if reset from within content */ this.resetTask = function (moveFocus = false) { for (let i = 0; i < params.answers.length; i++) { if (params.answers[i].checked) { delete params.answers[i].checked; } } self.answered = false; self.hideSolutions(); params.userAnswers = []; removeSelections(moveFocus); self.showButton('check-answer'); self.hideButton('try-again'); self.hideButton('show-solution'); enableInput(); $myDom?.find('.h5p-feedback-available').remove(); }; var calculateMaxScore = function () { if (blankIsCorrect) { return params.weight; } var maxScore = 0; for (var i = 0; i < params.answers.length; i++) { var choice = params.answers[i]; if (choice.correct) { maxScore += (choice.weight !== undefined ? choice.weight : 1); } } return maxScore; }; this.getMaxScore = function () { return (!params.behaviour.singleAnswer && !params.behaviour.singlePoint ? calculateMaxScore() : params.weight); }; /** * Check answer */ var checkAnswer = function () { // Unbind removal of feedback dialogs on click $myDom.unbind('click', removeFeedbackDialog); // Remove all tip dialogs removeFeedbackDialog(); if (params.behaviour.enableSolutionsButton) { self.showButton('show-solution'); } if (params.behaviour.enableRetry) { self.showButton('try-again'); } self.hideButton('check-answer'); self.showCheckSolution(); disableInput(); var xAPIEvent = self.createXAPIEventTemplate('answered'); addQuestionToXAPI(xAPIEvent); addResponseToXAPI(xAPIEvent); self.trigger(xAPIEvent); }; /** * Adds the ui buttons. * @private */ var addButtons = function () { var $content = $('[data-content-id="' + self.contentId + '"].h5p-content'); var $containerParents = $content.parents('.h5p-container'); // select find container to attach dialogs to var $container; if($containerParents.length !== 0) { // use parent highest up if any $container = $containerParents.last(); } else if($content.length !== 0){ $container = $content; } else { $container = $(document.body); } // Show solution button self.addButton('show-solution', params.UI.showSolutionButton, function () { if (params.behaviour.showSolutionsRequiresInput && !self.getAnswerGiven(true)) { // Require answer before solution can be viewed self.updateFeedbackContent(params.UI.noInput); self.read(params.UI.noInput); } else { calcScore(); self.showAllSolutions(); } }, false, { 'aria-label': params.UI.a11yShowSolution, }); // Check button if (params.behaviour.enableCheckButton && (!params.behaviour.autoCheck || !params.behaviour.singleAnswer)) { self.addButton('check-answer', params.UI.checkAnswerButton, function () { self.answered = true; checkAnswer(); $myDom.find('.h5p-answer:first-child').focus(); }, true, { 'aria-label': params.UI.a11yCheck, }, { confirmationDialog: { enable: params.behaviour.confirmCheckDialog, l10n: params.confirmCheck, instance: self, $parentElement: $container }, contentData: self.contentData, textIfSubmitting: params.UI.submitAnswerButton, } ); } // Try Again button self.addButton('try-again', params.UI.tryAgainButton, function () { self.resetTask(true); if (params.behaviour.randomAnswers) { // reshuffle answers var oldIdMap = idMap; idMap = getShuffleMap(); var answersDisplayed = $myDom.find('.h5p-answer'); // remember tips var tip = []; for (i = 0; i < answersDisplayed.length; i++) { tip[i] = $(answersDisplayed[i]).find('.h5p-multichoice-tipwrap'); } // Those two loops cannot be merged or you'll screw up your tips for (i = 0; i < answersDisplayed.length; i++) { // move tips and answers on display $(answersDisplayed[i]).find('.h5p-alternative-inner').html(params.answers[i].text); $(tip[i]).detach().appendTo($(answersDisplayed[idMap.indexOf(oldIdMap[i])]).find('.h5p-alternative-container')); } } }, false, { 'aria-label': params.UI.a11yRetry, }, { confirmationDialog: { enable: params.behaviour.confirmRetryDialog, l10n: params.confirmRetry, instance: self, $parentElement: $container } }); }; /** * Determine which feedback text to display * * @param {number} score * @param {number} max * @return {string} */ var getFeedbackText = function (score, max) { var ratio = (score / max); var feedback = H5P.Question.determineOverallFeedback(params.overallFeedback, ratio); return feedback.replace('@score', score).replace('@total', max); }; /** * Shows feedback on the selected fields. * @public * @param {boolean} [skipFeedback] Skip showing feedback if true */ this.showCheckSolution = function (skipFeedback) { var scorePoints; if (!(params.behaviour.singleAnswer || params.behaviour.singlePoint || !params.behaviour.showScorePoints)) { scorePoints = new H5P.Question.ScorePoints(); } $myDom.find('.h5p-answer').each(function (i, e) { var $e = $(e); var a = params.answers[i]; var chosen = ($e.attr('aria-checked') === 'true'); if (chosen) { if (a.correct) { // May already have been applied by instant feedback if (!$e.hasClass('h5p-correct')) { $e.addClass('h5p-correct').append($('<span/>', { 'class': 'h5p-answer-icon', html: params.UI.correctAnswer + '.' })); } } else { if (!$e.hasClass('h5p-wrong')) { $e.addClass('h5p-wrong').append($('<span/>', { 'class': 'h5p-answer-icon', html: params.UI.wrongAnswer + '.' })); } } if (scorePoints) { var alternativeContainer = $e[0].querySelector('.h5p-alternative-container'); if (!params.behaviour.autoCheck || alternativeContainer.querySelector('.h5p-question-plus-one, .h5p-question-minus-one') === null) { alternativeContainer.appendChild(scorePoints.getElement(a.correct)); } } } if (!skipFeedback) { if (chosen && a.tipsAndFeedback.chosenFeedback !== undefined && a.tipsAndFeedback.chosenFeedback !== '') { addFeedback($e, a.tipsAndFeedback.chosenFeedback); } else if (!chosen && a.tipsAndFeedback.notChosenFeedback !== undefined && a.tipsAndFeedback.notChosenFeedback !== '') { addFeedback($e, a.tipsAndFeedback.notChosenFeedback); } } }); // Determine feedback var max = self.getMaxScore(); // Disable task if maxscore is achieved var fullScore = (score === max); if (fullScore) { self.hideButton('check-answer'); self.hideButton('try-again'); self.hideButton('show-solution'); } // Show feedback if (!skipFeedback) { this.setFeedback(getFeedbackText(score, max), score, max, params.UI.scoreBarLabel); } self.trigger('resize'); }; /** * Disables choosing new input. */ var disableInput = function () { $('.h5p-answer', $myDom).attr({ 'aria-disabled': 'true', 'tabindex': '-1' }).removeAttr('role') .removeAttr('aria-checked'); $('.h5p-answers').removeAttr('role'); }; /** * Enables new input. */ var enableInput = function () { $('.h5p-answer', $myDom) .attr({ 'aria-disabled': 'false', 'role': params.behaviour.singleAnswer ? 'radio' : 'checkbox', }); $('.h5p-answers').attr('role', params.role); }; var calcScore = function () { score = 0; for (const answer of params.userAnswers) { const choice = params.answers[answer]; const weight = (choice.weight !== undefined ? choice.weight : 1); if (choice.correct) { score += weight; } else { score -= weight; } } if (score < 0) { score = 0; } if (!params.userAnswers.length && blankIsCorrect) { score = params.weight; } if (params.behaviour.singlePoint) { score = (100 * score / calculateMaxScore()) >= params.behaviour.passPercentage ? params.weight : 0; } }; /** * Removes selections from task. */ var removeSelections = function (moveFocus) { var $answers = $('.h5p-answer', $myDom) .removeClass('h5p-selected') .attr('aria-checked', 'false'); if (!params.behaviour.singleAnswer) { $answers.attr('tabindex', '0'); } else { $answers.first().attr('tabindex', '0'); } // Set focus to first option if (moveFocus || self.isRoot()) { $answers.first().focus(); } calcScore(); }; /** * Get xAPI data. * Contract used by report rendering engine. * * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6} */ this.getXAPIData = function(){ var xAPIEvent = this.createXAPIEventTemplate('answered'); addQuestionToXAPI(xAPIEvent); addResponseToXAPI(xAPIEvent); return { statement: xAPIEvent.data.statement }; }; /** * Add the question itself to the definition part of an xAPIEvent */ var addQuestionToXAPI = function (xAPIEvent) { var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']); definition.description = { 'en-US': params.question, }; definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction'; definition.interactionType = 'choice'; definition.correctResponsesPattern = []; definition.choices = []; for (var i = 0; i < params.answers.length; i++) { definition.choices[i] = { 'id': params.answers[i].originalOrder + '', 'description': { // Remove tags, must wrap in div tag because jQuery 1.9 will crash if the string isn't wrapped in a tag. 'en-US': $('<div>' + params.answers[i].text + '</div>').text() } }; if (params.answers[i].correct) { if (!params.singleAnswer) { if (definition.correctResponsesPattern.length) { definition.correctResponsesPattern[0] += '[,]'; // This looks insane, but it's how you separate multiple answers // that must all be chosen to achieve perfect score... } else { definition.correctResponsesPattern.push(''); } definition.correctResponsesPattern[0] += params.answers[i].originalOrder; } else { definition.correctResponsesPattern.push('' + params.answers[i].originalOrder); } } } }; /** * Add the response part to an xAPI event * * @param {H5P.XAPIEvent} xAPIEvent * The xAPI event we will add a response to */ var addResponseToXAPI = function (xAPIEvent) { var maxScore = self.getMaxScore(); var success = (100 * score / maxScore) >= params.behaviour.passPercentage; xAPIEvent.setScoredResult(score, maxScore, self, true, success); if (params.userAnswers === undefined) { calcScore(); } // Add the response var response = ''; for (var i = 0; i < params.userAnswers.length; i++) { if (response !== '') { response += '[,]'; } response += idMap === undefined ? params.userAnswers[i] : idMap[params.userAnswers[i]]; } xAPIEvent.data.statement.result.response = response; }; /** * Create a map pointing from original answers to shuffled answers * * @return {number[]} map pointing from original answers to shuffled answers */ var getShuffleMap = function() { params.answers = H5P.shuffleArray(params.answers); // Create a map from the new id to the old one var idMap = []; for (i = 0; i < params.answers.length; i++) { idMap[i] = params.answers[i].originalOrder; } return idMap; }; // Initialization code // Randomize order, if requested var idMap; // Store original order in answers for (i = 0; i < params.answers.length; i++) { params.answers[i].originalOrder = i; } if (params.behaviour.randomAnswers) { idMap = getShuffleMap(); } // Start with an empty set of user answers. params.userAnswers = []; // Restore previous state if (contentData && contentData.previousState !== undefined) { // Restore answers if (contentData.previousState.answers) { if (!idMap) { params.userAnswers = contentData.previousState.answers; } else { // The answers have been shuffled, and we must use the id mapping. for (i = 0; i < contentData.previousState.answers.length; i++) { for (var k = 0; k < idMap.length; k++) { if (idMap[k] === contentData.previousState.answers[i]) { params.userAnswers.push(k); } } } } calcScore(); } } var hasCheckedAnswer = false; // Loop through choices for (var j = 0; j < params.answers.length; j++) { var ans = params.answers[j]; if (!params.behaviour.singleAnswer) { // Set role ans.role = 'checkbox'; ans.tabindex = '0'; if (params.userAnswers.indexOf(j) !== -1) { ans.checked = 'true'; hasCheckedAnswer = true; } } else { // Set role ans.role = 'radio'; // Determine tabindex, checked and extra classes if (params.userAnswers.length === 0) { // No correct answers if (i === 0 || i === params.answers.length) { ans.tabindex = '0'; } } else if (params.userAnswers.indexOf(j) !== -1) { // This is the correct choice ans.tabindex = '0'; ans.checked = 'true'; hasCheckedAnswer = true; } } // Set default if (ans.tabindex === undefined) { ans.tabindex = '-1'; } if (ans.checked === undefined) { ans.checked = 'false'; } } H5P.MultiChoice.counter = (H5P.MultiChoice.counter === undefined ? 0 : H5P.MultiChoice.counter + 1); params.role = (params.behaviour.singleAnswer ? 'radiogroup' : 'group'); params.labelId = 'h5p-mcq' + H5P.MultiChoice.counter; /** * Pack the current state of the interactivity into a object that can be * serialized. * * @public */ this.getCurrentState = function () { var state = {}; if (!idMap) { state.answers = params.userAnswers; } else { // The answers have been shuffled and must be mapped back to their // original ID. state.answers = []; for (var i = 0; i < params.userAnswers.length; i++) { state.answers.push(idMap[params.userAnswers[i]]); } } return state; }; /** * Check if user has given an answer. * * @param {boolean} [ignoreCheck] Ignore returning true from pressing "check-answer" button. * @return {boolean} True if answer is given */ this.getAnswerGiven = function (ignoreCheck) { var answered = ignoreCheck ? false : this.answered; return answered || params.userAnswers.length > 0 || blankIsCorrect; }; this.getScore = function () { return score; }; this.getTitle = function () { return H5P.createTitle((this.contentData && this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title : 'Multiple Choice'); }; }; H5P.MultiChoice.prototype = Object.create(H5P.Question.prototype); H5P.MultiChoice.prototype.constructor = H5P.MultiChoice; ; /** @namespace H5P */ H5P.VideoVimeo = (function ($) { let numInstances = 0; /** * Vimeo video player for H5P. * * @class * @param {Array} sources Video files to use * @param {Object} options Settings for the player * @param {Object} l10n Localization strings */ function VimeoPlayer(sources, options, l10n) { const self = this; let player; // Since all the methods of the Vimeo Player SDK are promise-based, we keep // track of all relevant state variables so that we can implement the // H5P.Video API where all methods return synchronously. let buffered = 0; let currentQuality; let currentTextTrack; let currentTime = 0; let duration = 0; let isMuted = 0; let volume = 0; let playbackRate = 1; let qualities = []; let loadingFailedTimeout; let failedLoading = false; let ratio = 9/16; let isLoaded = false; const LOADING_TIMEOUT_IN_SECONDS = 8; const id = `h5p-vimeo-${++numInstances}`; const $wrapper = $('<div/>'); const $placeholder = $('<div/>', { id: id, html: `<div class="h5p-video-loading" style="height: 100%; min-height: 200px; display: block; z-index: 100;" aria-label="${l10n.loading}"></div>` }).appendTo($wrapper); /** * Create a new player with the Vimeo Player SDK. * * @private */ const createVimeoPlayer = async () => { if (!$placeholder.is(':visible') || player !== undefined) { return; } // Since the SDK is loaded asynchronously below, explicitly set player to // null (unlike undefined) which indicates that creation has begun. This // allows the guard statement above to be hit if this function is called // more than once. player = null; const Vimeo = await loadVimeoPlayerSDK(); const MIN_WIDTH = 200; const width = Math.max($wrapper.width(), MIN_WIDTH); const canHasControls = options.controls || self.pressToPlay; const embedOptions = { url: sources[0].path, controls: canHasControls, responsive: true, dnt: true, // Hardcoded autoplay to false to avoid playing videos on init autoplay: false, loop: options.loop ? true : false, playsinline: true, quality: 'auto', width: width, muted: false, keyboard: canHasControls, }; // Create a new player player = new Vimeo.Player(id, embedOptions); registerVimeoPlayerEventListeneners(player); // Failsafe timeout to handle failed loading of videos. // This seems to happen for private videos even though the SDK docs // suggests to catch PrivacyError when attempting play() loadingFailedTimeout = setTimeout(() => { failedLoading = true; removeLoadingIndicator(); $wrapper.html(`<p class="vimeo-failed-loading">${l10n.vimeoLoadingError}</p>`); $wrapper.css({ width: null, height: null }); self.trigger('resize'); self.trigger('error', l10n.vimeoLoadingError); }, LOADING_TIMEOUT_IN_SECONDS * 1000); } const removeLoadingIndicator = () => { $placeholder.find('div.h5p-video-loading').remove(); }; /** * Register event listeners on the given Vimeo player. * * @private * @param {Vimeo.Player} player */ const registerVimeoPlayerEventListeneners = (player) => { let isFirstPlay, tracks; player.on('loaded', async () => { isFirstPlay = true; isLoaded = true; clearTimeout(loadingFailedTimeout); const videoDetails = await getVimeoVideoMetadata(player); tracks = videoDetails.tracks.options; currentTextTrack = tracks.current; duration = videoDetails.duration; qualities = videoDetails.qualities; currentQuality = 'auto'; try { ratio = videoDetails.dimensions.height / videoDetails.dimensions.width; } catch (e) { /* Intentionally ignore this, and fallback on the default ratio */ } removeLoadingIndicator(); if (options.startAt) { // Vimeo.Player doesn't have an option for setting start time upon // instantiation, so we instead perform an initial seek here. currentTime = await self.seek(options.startAt); } self.trigger('ready'); self.trigger('loaded'); self.trigger('qualityChange', currentQuality); self.trigger('resize'); }); player.on('play', () => { if (isFirstPlay) { isFirstPlay = false; if (tracks.length) { self.trigger('captions', tracks); } } }); // Handle playback state changes. player.on('playing', () => self.trigger('stateChange', H5P.Video.PLAYING)); player.on('pause', () => self.trigger('stateChange', H5P.Video.PAUSED)); player.on('ended', () => self.trigger('stateChange', H5P.Video.ENDED)); // Track the percentage of video that has finished loading (buffered). player.on('progress', (data) => { buffered = data.percent * 100; }); // Track the current time. The update frequency may be browser-dependent, // according to the official docs: // https://developer.vimeo.com/player/sdk/reference#timeupdate player.on('timeupdate', (time) => { currentTime = time.seconds; }); }; /** * Get metadata about the video loaded in the given Vimeo player. * * Example resolved value: * * ``` * { * "duration": 39, * "qualities": [ * { * "name": "auto", * "label": "Auto" * }, * { * "name": "1080p", * "label": "1080p" * }, * { * "name": "720p", * "label": "720p" * } * ], * "dimensions": { * "width": 1920, * "height": 1080 * }, * "tracks": { * "current": { * "label": "English", * "value": "en" * }, * "options": [ * { * "label": "English", * "value": "en" * }, * { * "label": "Norsk bokmål", * "value": "nb" * } * ] * } * } * ``` * * @private * @param {Vimeo.Player} player * @returns {Promise} */ const getVimeoVideoMetadata = (player) => { // Create an object for easy lookup of relevant metadata const massageVideoMetadata = (data) => { const duration = data[0]; const qualities = data[1].map(q => ({ name: q.id, label: q.label })); const tracks = data[2].reduce((tracks, current) => { const h5pVideoTrack = new H5P.Video.LabelValue(current.label, current.language); tracks.options.push(h5pVideoTrack); if (current.mode === 'showing') { tracks.current = h5pVideoTrack; } return tracks; }, { current: undefined, options: [] }); const dimensions = { width: data[3], height: data[4] }; return { duration, qualities, tracks, dimensions }; }; return Promise.all([ player.getDuration(), player.getQualities(), player.getTextTracks(), player.getVideoWidth(), player.getVideoHeight(), ]).then(data => massageVideoMetadata(data)); } try { if (document.featurePolicy.allowsFeature('autoplay') === false) { self.pressToPlay = true; } } catch (err) {} /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ self.appendTo = ($container) => { $container.addClass('h5p-vimeo').append($wrapper); createVimeoPlayer(); }; /** * Get list of available qualities. * * @public * @returns {Array} */ self.getQualities = () => { return qualities; }; /** * Get the current quality. * * @returns {String} Current quality identifier */ self.getQuality = () => { return currentQuality; }; /** * Set the playback quality. * * @public * @param {String} quality */ self.setQuality = async (quality) => { currentQuality = await player.setQuality(quality); self.trigger('qualityChange', currentQuality); }; /** * Start the video. * * @public */ self.play = async () => { if (!player) { self.on('ready', self.play); return; } try { await player.play(); } catch (error) { switch (error.name) { case 'PasswordError': // The video is password-protected self.trigger('error', l10n.vimeoPasswordError); break; case 'PrivacyError': // The video is private self.trigger('error', l10n.vimeoPrivacyError); break; default: self.trigger('error', l10n.unknownError); break; } } }; /** * Pause the video. * * @public */ self.pause = () => { if (player) { player.pause(); } }; /** * Seek video to given time. * * @public * @param {Number} time */ self.seek = async (time) => { if (!player) { return; } currentTime = time; await player.setCurrentTime(time); }; /** * @public * @returns {Number} Seconds elapsed since beginning of video */ self.getCurrentTime = () => { return currentTime; }; /** * @public * @returns {Number} Video duration in seconds */ self.getDuration = () => { return duration; }; /** * Get percentage of video that is buffered. * * @public * @returns {Number} Between 0 and 100 */ self.getBuffered = () => { return buffered; }; /** * Mute the video. * * @public */ self.mute = async () => { isMuted = await player.setMuted(true); }; /** * Unmute the video. * * @public */ self.unMute = async () => { isMuted = await player.setMuted(false); }; /** * Whether the video is muted. * * @public * @returns {Boolean} True if the video is muted, false otherwise */ self.isMuted = () => { return isMuted; }; /** * Whether the video is loaded. * * @public * @returns {Boolean} True if the video is muted, false otherwise */ self.isLoaded = () => { return isLoaded; }; /** * Get the video player's current sound volume. * * @public * @returns {Number} Between 0 and 100. */ self.getVolume = () => { return volume; }; /** * Set the video player's sound volume. * * @public * @param {Number} level */ self.setVolume = async (level) => { volume = await player.setVolume(level); }; /** * Get list of available playback rates. * * @public * @returns {Array} Available playback rates */ self.getPlaybackRates = () => { return [0.5, 1, 1.5, 2]; }; /** * Get the current playback rate. * * @public * @returns {Number} e.g. 0.5, 1, 1.5 or 2 */ self.getPlaybackRate = () => { return playbackRate; }; /** * Set the current playback rate. * * @public * @param {Number} rate Must be one of available rates from getPlaybackRates */ self.setPlaybackRate = async (rate) => { playbackRate = await player.setPlaybackRate(rate); self.trigger('playbackRateChange', rate); }; /** * Set current captions track. * * @public * @param {H5P.Video.LabelValue} track Captions to display */ self.setCaptionsTrack = (track) => { if (!track) { return player.disableTextTrack().then(() => { currentTextTrack = null; }); } player.enableTextTrack(track.value).then(() => { currentTextTrack = track; }); }; /** * Get current captions track. * * @public * @returns {H5P.Video.LabelValue} */ self.getCaptionsTrack = () => { return currentTextTrack; }; self.on('resize', () => { if (failedLoading || !$wrapper.is(':visible')) { return; } if (player === undefined) { // Player isn't created yet. Try again. createVimeoPlayer(); return; } // Use as much space as possible $wrapper.css({ width: '100%', height: 'auto' }); const width = $wrapper[0].clientWidth; const height = options.fit ? $wrapper[0].clientHeight : (width * (ratio)); // Validate height before setting if (height > 0) { // Set size $wrapper.css({ width: width + 'px', height: height + 'px' }); } }); } /** * Check to see if we can play any of the given sources. * * @public * @static * @param {Array} sources * @returns {Boolean} */ VimeoPlayer.canPlay = (sources) => { return getId(sources[0].path); }; /** * Find id of Vimeo video from given URL. * * @private * @param {String} url * @returns {String} Vimeo video ID */ const getId = (url) => { // https://stackoverflow.com/a/11660798 const matches = url.match(/^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/); if (matches && matches[5]) { return matches[5]; } }; /** * Load the Vimeo Player SDK asynchronously. * * @private * @returns {Promise} Vimeo Player SDK object */ const loadVimeoPlayerSDK = async () => { if (window.Vimeo) { return await Promise.resolve(window.Vimeo); } return await new Promise((resolve, reject) => { const tag = document.createElement('script'); tag.src = 'https://player.vimeo.com/api/player.js'; tag.onload = () => resolve(window.Vimeo); tag.onerror = reject; document.querySelector('script').before(tag); }); }; return VimeoPlayer; })(H5P.jQuery); // Register video handler H5P.videoHandlers = H5P.videoHandlers || []; H5P.videoHandlers.push(H5P.VideoVimeo); ; /** @namespace H5P */ H5P.VideoYouTube = (function ($) { /** * YouTube video player for H5P. * * @class * @param {Array} sources Video files to use * @param {Object} options Settings for the player * @param {Object} l10n Localization strings */ function YouTube(sources, options, l10n) { var self = this; var player; var playbackRate = 1; var id = 'h5p-youtube-' + numInstances; numInstances++; var ratio = 9/16; var $wrapper = $('<div/>'); var $placeholder = $('<div />', { text: l10n.loading, html: `<div style="position: relative; padding: ${ratio*100}% 0 0 0;"><div id="${id}"></div></div>` }).appendTo($wrapper); // Optional placeholder // var $placeholder = $('<iframe id="' + id + '" type="text/html" width="640" height="360" src="https://www.youtube.com/embed/' + getId(sources[0].path) + '?enablejsapi=1&origin=' + encodeURIComponent(ORIGIN) + '&autoplay=' + (options.autoplay ? 1 : 0) + '&controls=' + (options.controls ? 1 : 0) + '&disabledkb=' + (options.controls ? 0 : 1) + '&fs=0&loop=' + (options.loop ? 1 : 0) + '&rel=0&showinfo=0&iv_load_policy=3" frameborder="0"></iframe>').appendTo($wrapper); /** * Use the YouTube API to create a new player * * @private */ var create = function () { if (!$placeholder.is(':visible') || player !== undefined) { return; } if (window.YT === undefined) { // Load API first loadAPI(create); return; } if (YT.Player === undefined) { return; } var width = $wrapper.width(); if (width < 200) { width = 200; } var loadCaptionsModule = true; var videoId = getId(sources[0].path); player = new YT.Player(id, { width: width, height: width * (9/16), videoId: videoId, playerVars: { origin: ORIGIN, // Hardcoded autoplay to false to avoid playing videos on init autoplay: 0, controls: options.controls ? 1 : 0, disablekb: options.controls ? 0 : 1, fs: 0, loop: options.loop ? 1 : 0, playlist: options.loop ? videoId : undefined, rel: 0, showinfo: 0, iv_load_policy: 3, wmode: "opaque", start: Math.floor(options.startAt), playsinline: 1 }, events: { onReady: function () { self.trigger('ready'); self.trigger('loaded'); if (!options.autoplay) { self.toPause = true; } if (options.deactivateSound) { self.mute(); } }, onApiChange: function () { if (loadCaptionsModule) { loadCaptionsModule = false; // Always load captions player.loadModule('captions'); } var trackList; try { // Grab tracklist from player trackList = player.getOption('captions', 'tracklist'); } catch (err) {} if (trackList && trackList.length) { // Format track list into valid track options var trackOptions = []; for (var i = 0; i < trackList.length; i++) { trackOptions.push(new H5P.Video.LabelValue(trackList[i].displayName, trackList[i].languageCode)); } // Captions are ready for loading self.trigger('captions', trackOptions); } }, onStateChange: function (state) { if (state.data > -1 && state.data < 4) { if (self.toPause) { // if video buffering, was likely paused already - skip if (state.data === H5P.Video.BUFFERING) { delete self.toPause; } else { self.pause(); } } // Fix for keeping playback rate in IE11 if (H5P.Video.IE11_PLAYBACK_RATE_FIX && state.data === H5P.Video.PLAYING && playbackRate !== 1) { // YT doesn't know that IE11 changed the rate so it must be reset before it's set to the correct value player.setPlaybackRate(1); player.setPlaybackRate(playbackRate); } // End IE11 fix self.trigger('stateChange', state.data); } }, onPlaybackQualityChange: function (quality) { self.trigger('qualityChange', quality.data); }, onPlaybackRateChange: function (playbackRate) { self.trigger('playbackRateChange', playbackRate.data); }, onError: function (error) { var message; switch (error.data) { case 2: message = l10n.invalidYtId; break; case 100: message = l10n.unknownYtId; break; case 101: case 150: message = l10n.restrictedYt; break; default: message = l10n.unknownError + ' ' + error.data; break; } self.trigger('error', message); } } }); player.g.style = "position:absolute;top:0;left:0;width:100%;height:100%;"; }; /** * Indicates if the video must be clicked for it to start playing. * For instance YouTube videos on iPad must be pressed to start playing. * * @public */ if (navigator.userAgent.match(/iPad/i)) { self.pressToPlay = true; } else { try { if (document.featurePolicy.allowsFeature('autoplay') === false) { self.pressToPlay = true; } } catch (err) {} } /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ self.appendTo = function ($container) { $container.addClass('h5p-youtube').append($wrapper); create(); }; /** * Get list of available qualities. Not available until after play. * * @public * @returns {Array} */ self.getQualities = function () { if (!player || !player.getAvailableQualityLevels) { return; } var qualities = player.getAvailableQualityLevels(); if (!qualities.length) { return; // No qualities } // Add labels for (var i = 0; i < qualities.length; i++) { var quality = qualities[i]; var label = (LABELS[quality] !== undefined ? LABELS[quality] : 'Unknown'); // TODO: l10n qualities[i] = { name: quality, label: LABELS[quality] }; } return qualities; }; /** * Get current playback quality. Not available until after play. * * @public * @returns {String} */ self.getQuality = function () { if (!player || !player.getPlaybackQuality) { return; } var quality = player.getPlaybackQuality(); return quality === 'unknown' ? undefined : quality; }; /** * Set current playback quality. Not available until after play. * Listen to event "qualityChange" to check if successful. * * @public * @params {String} [quality] */ self.setQuality = function (quality) { if (!player || !player.setPlaybackQuality) { return; } player.setPlaybackQuality(quality); }; /** * Start the video. * * @public */ self.play = function () { if (!player || !player.playVideo) { self.on('ready', self.play); return; } player.playVideo(); }; /** * Pause the video. * * @public */ self.pause = function () { delete self.toPause; self.off('ready', self.play); if (!player || !player.pauseVideo) { return; } player.pauseVideo(); }; /** * Seek video to given time. * * @public * @param {Number} time */ self.seek = function (time) { if (!player || !player.seekTo) { return; } player.seekTo(time, true); }; /** * Recreate player with initial time * * @public * @param {Number} time */ self.resetPlayback = function (time) { options.startAt = time; if (player) { if (player.getPlayerState() === H5P.Video.PLAYING) { player.pauseVideo(); self.trigger('stateChange', H5P.Video.PAUSED); } player.destroy(); player = undefined; } create(); } /** * Get elapsed time since video beginning. * * @public * @returns {Number} */ self.getCurrentTime = function () { if (!player || !player.getCurrentTime) { return; } return player.getCurrentTime(); }; /** * Get total video duration time. * * @public * @returns {Number} */ self.getDuration = function () { if (!player || !player.getDuration) { return; } return player.getDuration(); }; /** * Get percentage of video that is buffered. * * @public * @returns {Number} Between 0 and 100 */ self.getBuffered = function () { if (!player || !player.getVideoLoadedFraction) { return; } return player.getVideoLoadedFraction() * 100; }; /** * Turn off video sound. * * @public */ self.mute = function () { if (!player || !player.mute) { return; } player.mute(); }; /** * Turn on video sound. * * @public */ self.unMute = function () { if (!player || !player.unMute) { return; } player.unMute(); }; /** * Check if video sound is turned on or off. * * @public * @returns {Boolean} */ self.isMuted = function () { if (!player || !player.isMuted) { return; } return player.isMuted(); }; /** * Check if video is loaded and ready to play. * * @public * @returns {Boolean} */ self.isLoaded = function () { if (!player || !player.getPlayerState) { return; } return player.getPlayerState() === 5; }; /** * Return the video sound level. * * @public * @returns {Number} Between 0 and 100. */ self.getVolume = function () { if (!player || !player.getVolume) { return; } return player.getVolume(); }; /** * Set video sound level. * * @public * @param {Number} level Between 0 and 100. */ self.setVolume = function (level) { if (!player || !player.setVolume) { return; } player.setVolume(level); }; /** * Get list of available playback rates. * * @public * @returns {Array} available playback rates */ self.getPlaybackRates = function () { if (!player || !player.getAvailablePlaybackRates) { return; } var playbackRates = player.getAvailablePlaybackRates(); if (!playbackRates.length) { return; // No rates, but the array should contain at least 1 } return playbackRates; }; /** * Get current playback rate. * * @public * @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2 */ self.getPlaybackRate = function () { if (!player || !player.getPlaybackRate) { return; } return player.getPlaybackRate(); }; /** * Set current playback rate. * Listen to event "playbackRateChange" to check if successful. * * @public * @params {Number} suggested rate that may be rounded to supported values */ self.setPlaybackRate = function (newPlaybackRate) { if (!player || !player.setPlaybackRate) { return; } playbackRate = Number(newPlaybackRate); player.setPlaybackRate(playbackRate); }; /** * Set current captions track. * * @param {H5P.Video.LabelValue} Captions track to show during playback */ self.setCaptionsTrack = function (track) { player.setOption('captions', 'track', track ? {languageCode: track.value} : {}); }; /** * Figure out which captions track is currently used. * * @return {H5P.Video.LabelValue} Captions track */ self.getCaptionsTrack = function () { var track = player.getOption('captions', 'track'); return (track.languageCode ? new H5P.Video.LabelValue(track.displayName, track.languageCode) : null); }; // Respond to resize events by setting the YT player size. self.on('resize', function () { if (!$wrapper.is(':visible')) { return; } if (!player) { // Player isn't created yet. Try again. create(); return; } // Use as much space as possible $wrapper.css({ width: '100%', height: 'auto' }); var width = $wrapper[0].clientWidth; var height = options.fit ? $wrapper[0].clientHeight : (width * (9/16)); // Validate height before setting if (height > 0) { // Set size $wrapper.css({ width: width + 'px', height: height + 'px' }); player.setSize(width, height); } }); } /** * Check to see if we can play any of the given sources. * * @public * @static * @param {Array} sources * @returns {Boolean} */ YouTube.canPlay = function (sources) { return getId(sources[0].path); }; /** * Find id of YouTube video from given URL. * * @private * @param {String} url * @returns {String} YouTube video identifier */ var getId = function (url) { // Has some false positives, but should cover all regular URLs that people can find var matches = url.match(/(?:(?:youtube.com\/(?:attribution_link\?(?:\S+))?(?:v\/|embed\/|watch\/|(?:user\/(?:\S+)\/)?watch(?:\S+)v\=))|(?:youtu.be\/|y2u.be\/))([A-Za-z0-9_-]{11})/i); if (matches && matches[1]) { return matches[1]; } }; /** * Load the IFrame Player API asynchronously. */ var loadAPI = function (loaded) { if (window.onYouTubeIframeAPIReady !== undefined) { // Someone else is loading, hook in var original = window.onYouTubeIframeAPIReady; window.onYouTubeIframeAPIReady = function (id) { loaded(id); original(id); }; } else { // Load the API our self var tag = document.createElement('script'); tag.src = "https://www.youtube.com/iframe_api"; var firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); window.onYouTubeIframeAPIReady = loaded; } }; /** @constant {Object} */ var LABELS = { highres: '2160p', // Old API support hd2160: '2160p', // (New API) hd1440: '1440p', hd1080: '1080p', hd720: '720p', large: '480p', medium: '360p', small: '240p', tiny: '144p', auto: 'Auto' }; /** @private */ var numInstances = 0; // Extract the current origin (used for security) var ORIGIN = window.location.href.match(/http[s]?:\/\/[^\/]+/); ORIGIN = !ORIGIN || ORIGIN[0] === undefined ? undefined : ORIGIN[0]; // ORIGIN = undefined is needed to support fetching file from device local storage return YouTube; })(H5P.jQuery); // Register video handler H5P.videoHandlers = H5P.videoHandlers || []; H5P.videoHandlers.push(H5P.VideoYouTube); ; /** @namespace H5P */ H5P.VideoPanopto = (function ($) { /** * Panopto video player for H5P. * * @class * @param {Array} sources Video files to use * @param {Object} options Settings for the player * @param {Object} l10n Localization strings */ function Panopto(sources, options, l10n) { var self = this; self.volume = 100; self.toSeek = undefined; var player; var playbackRate = 1; let canHasAutoplay; var id = 'h5p-panopto-' + numInstances; numInstances++; let isLoaded = false; let isPlayerReady = false; var $wrapper = $('<div/>'); var $placeholder = $('<div/>', { id: id, html: '<div>' + l10n.loading + '</div>' }).appendTo($wrapper); // Determine autoplay/play. try { if (document.featurePolicy.allowsFeature('autoplay') !== false) { canHasAutoplay = true; } } catch (err) {} /** * Use the Panopto API to create a new player * * @private */ var create = function () { if (!$placeholder.is(':visible') || player !== undefined) { return; } if (window.EmbedApi === undefined) { // Load API first loadAPI(create); return; } var width = $wrapper.width(); if (width < 200) { width = 200; } const videoId = getId(sources[0].path); player = new EmbedApi(id, { width: width, height: width * (9/16), serverName: videoId[0], sessionId: videoId[1], videoParams: { // Optional interactivity: 'none', showtitle: false, autohide: true, offerviewer: false, autoplay: false, showbrand: false, start: 0, hideoverlay: !options.controls, }, events: { onIframeReady: function () { isPlayerReady = true; $placeholder.children(0).text(''); if (options.autoplay && canHasAutoplay) { player.loadVideo(); isLoaded = true; } self.trigger('containerLoaded'); self.trigger('resize'); // Avoid black iframe if loading is slow }, onReady: function () { self.videoLoaded = true; self.trigger('loaded'); if (typeof self.oldTime === 'number') { self.seek(self.oldTime); } else if (typeof self.startAt === 'number' && self.startAt > 0) { self.seek(self.startAt); } if (player.hasCaptions()) { const captions = []; const captionTracks = player.getCaptionTracks(); for (trackIndex in captionTracks) { captions.push(new H5P.Video.LabelValue(captionTracks[trackIndex], trackIndex)); } // Select active track currentTrack = player.getSelectedCaptionTrack(); currentTrack = captions[currentTrack] ? captions[currentTrack] : null; self.trigger('captions', captions); } }, onStateChange: function (state) { if ([H5P.Video.PLAYING, H5P.Video.PAUSED].includes(state) && typeof self.seekToTime === 'number') { player.seekTo(self.seekToTime); delete self.seekToTime; } // since panopto has different load sequence in IV, need additional condition here if (self.WAS_RESET) { self.WAS_RESET = false; } // TODO: Playback rate fix for IE11? if (state > -1 && state < 4) { self.trigger('stateChange', state); } }, onPlaybackRateChange: function () { self.trigger('playbackRateChange', self.getPlaybackRate()); }, onError: function (error) { if (error === ApiError.PlayWithSoundNotAllowed) { // pause and allow user to handle playing self.pause(); self.unMute(); // because player is automuted on this error } else { self.trigger('error', l10n.unknownError); } }, onLoginShown: function () { $placeholder.children().first().remove(); // Remove loading message self.trigger('loaded'); // Resize parent } } }); }; /** * Indicates if the video must be clicked for it to start playing. * This is always true for Panopto since all videos auto play. * * @public */ self.pressToPlay = true; /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ self.appendTo = function ($container) { $container.addClass('h5p-panopto').append($wrapper); create(); }; /** * Get list of available qualities. Not available until after play. * * @public * @returns {Array} */ self.getQualities = function () { // Not available for Panopto }; /** * Get current playback quality. Not available until after play. * * @public * @returns {String} */ self.getQuality = function () { // Not available for Panopto }; /** * Set current playback quality. Not available until after play. * Listen to event "qualityChange" to check if successful. * * @public * @params {String} [quality] */ self.setQuality = function (quality) { // Not available for Panopto }; /** * Start the video. * * @public */ self.play = function () { if (!player || !player.playVideo || !isPlayerReady) { return; } if (isLoaded || self.videoLoaded) { player.playVideo(); } else { player.loadVideo(); // Loads and starts playing isLoaded = true; } }; /** * Pause the video. * * @public */ self.pause = function () { if (!player || !player.pauseVideo) { return; } try { player.pauseVideo(); } catch (err) { // Swallow Panopto throwing an error. This has been seen in the authoring // tool if Panopto has been used inside Iv inside CP } }; /** * Seek video to given time. * * @public * @param {Number} time */ self.seek = function (time) { if (!player || !player.seekTo || !self.videoLoaded) { return; } if (!player.isReady) { self.seekToTime = time; return; } player.seekTo(time); if (self.WAS_RESET) { // need to check just to be sure, since state === 1 is unusable delete self.seekToTime; self.WAS_RESET = false; } }; /** * Recreate player with initial time * * @public * @param {Number} time */ self.resetPlayback = function (time) { if (player && player.isReady && self.videoLoaded) { self.seek(time); self.pause(); } else { self.seekToTime = time; } } /** * Get elapsed time since video beginning. * * @public * @returns {Number} */ self.getCurrentTime = function () { if (!player || !player.getCurrentTime) { return; } return player.getCurrentTime(); }; /** * Get total video duration time. * * @public * @returns {Number} */ self.getDuration = function () { if (!player || !player.getDuration) { return; } return player.getDuration(); }; /** * Get percentage of video that is buffered. * * @public * @returns {Number} Between 0 and 100 */ self.getBuffered = function () { // Not available for Panopto }; /** * Turn off video sound. * * @public */ self.mute = function () { if (!player || !player.muteVideo) { return; } player.muteVideo(); }; /** * Turn on video sound. * * @public */ self.unMute = function () { if (!player || !player.unmuteVideo) { return; } player.unmuteVideo(); // The volume is set to 0 when the browser prevents autoplay, // causing there to be no sound despite unmuting self.setVolume(self.volume); }; /** * Check if video sound is turned on or off. * * @public * @returns {Boolean} */ self.isMuted = function () { if (!player || !player.isMuted) { return; } return player.isMuted(); }; /** * Check video is loaded and ready to play * * @public * @returns {Boolean} */ self.isLoaded = function () { return isPlayerReady; }; /** * Return the video sound level. * * @public * @returns {Number} Between 0 and 100. */ self.getVolume = function () { if (!player || !player.getVolume) { return; } return player.getVolume() * 100; }; /** * Set video sound level. * * @public * @param {Number} level Between 0 and 100. */ self.setVolume = function (level) { if (!player || !player.setVolume) { return; } player.setVolume(level/100); self.volume = level; }; /** * Get list of available playback rates. * * @public * @returns {Array} available playback rates */ self.getPlaybackRates = function () { return [0.25, 0.5, 1, 1.25, 1.5, 2]; }; /** * Get current playback rate. * * @public * @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2 */ self.getPlaybackRate = function () { if (!player || !player.getPlaybackRate) { return; } return player.getPlaybackRate(); }; /** * Set current playback rate. * Listen to event "playbackRateChange" to check if successful. * * @public * @params {Number} suggested rate that may be rounded to supported values */ self.setPlaybackRate = function (newPlaybackRate) { if (!player || !player.setPlaybackRate) { return; } player.setPlaybackRate(newPlaybackRate); }; /** * Set current captions track. * * @param {H5P.Video.LabelValue} Captions track to show during playback */ self.setCaptionsTrack = function (track) { if (!track) { player.disableCaptions(); currentTrack = null; } else { player.enableCaptions(track.value + ''); currentTrack = track; } }; /** * Figure out which captions track is currently used. * * @return {H5P.Video.LabelValue} Captions track */ self.getCaptionsTrack = function () { return currentTrack; // No function for getting active caption track? }; // Respond to resize events by setting the player size. self.on('resize', function () { if (!$wrapper.is(':visible')) { return; } if (!player) { // Player isn't created yet. Try again. create(); return; } // Use as much space as possible $wrapper.css({ width: '100%', height: '100%' }); var width = $wrapper[0].clientWidth; var height = options.fit ? $wrapper[0].clientHeight : (width * (9/16)); // Set size $wrapper.css({ width: width + 'px', height: height + 'px' }); const $iframe = $placeholder.children('iframe'); if ($iframe.length) { $iframe.attr('width', width); $iframe.attr('height', height); } }); let currentTrack; } /** * Check to see if we can play any of the given sources. * * @public * @static * @param {Array} sources * @returns {Boolean} */ Panopto.canPlay = function (sources) { return getId(sources[0].path); }; /** * Find id of YouTube video from given URL. * * @private * @param {String} url * @returns {String} Panopto video identifier */ var getId = function (url) { const matches = url.match(/^[^\/]+:\/\/([^\/]*panopto\.[^\/]+)\/Panopto\/.+\?id=(.+)$/); if (matches && matches.length === 3) { return [matches[1], matches[2]]; } }; /** * Load the IFrame Player API asynchronously. */ var loadAPI = function (loaded) { if (window.onPanoptoEmbedApiReady !== undefined) { // Someone else is loading, hook in var original = window.onPanoptoEmbedApiReady; window.onPanoptoEmbedApiReady = function (id) { loaded(id); original(id); }; } else { // Load the API our self var tag = document.createElement('script'); tag.src = 'https://developers.panopto.com/scripts/embedapi.min.js'; var firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); window.onPanoptoEmbedApiReady = loaded; } }; /** @private */ var numInstances = 0; return Panopto; })(H5P.jQuery); // Register video handler H5P.videoHandlers = H5P.videoHandlers || []; H5P.videoHandlers.push(H5P.VideoPanopto); ; /** @namespace Echo */ H5P.VideoEchoVideo = (() => { let numInstances = 0; const CONTROLS_HEIGHT = 100; /** * EchoVideo video player for H5P. * * @class * @param {Array} sources Video files to use * @param {Object} options Settings for the player * @param {Object} l10n Localization strings */ function EchoPlayer(sources, options, l10n) { // State variables for the Player. let player = undefined; let buffered = 0; let currentQuality; let trackOptions = []; let currentTime = 0; let duration = 0; let isMuted = false; let loadingComplete = false; let volume = 1; let playbackRate = 1; let qualities = []; let loadingFailedTimeout; let failedLoading = false; let ratio = 9 / 16; let currentState = H5P.Video.VIDEO_CUED; // Echo360 server doesn't sync seek time with regular play time fast enough let timelineUpdatesToSkip = 0; let timeUpdateTimeout; /* * Echo360 player does send time updates ~ 0.25 seconds by default and * ends playing the video without sending a final time update or an * Video Ended event. We take care of determining reaching the video end * ourselves. */ const echoMinUncertaintyCompensationS = 0.3; const timelineUpdateDeltaSlackMS = 50; let echoUncertaintyCompensationS = echoMinUncertaintyCompensationS; let previousTickMS; // Player specific immutable variables. const LOADING_TIMEOUT_IN_SECONDS = 30; const id = `h5p-echo-${++numInstances}`; const instanceId = H5P.createUUID(); const wrapperElement = document.createElement('div'); const placeholderElement = document.createElement('div'); placeholderElement.classList.add('h5p-video-loading'); placeholderElement.setAttribute('style', 'height: 100%; min-height: 200px; display: block; z-index: 100; border: none;'); placeholderElement.setAttribute('aria-label', l10n.loading); wrapperElement.setAttribute('id', id); wrapperElement.append(placeholderElement); /** * Remove all elements from the placeholder dom element. * * @private */ const removeLoadingIndicator = () => { placeholderElement.replaceChildren(); }; /** * Generate an array of objects for use in a dropdown from the list of resolutions. * @private * @param {Array} qualityLevels - list of objects with supported qualities for the media * @returns {Array} list of objects with label and name properties */ const mapQualityLevels = (qualityLevels) => { const qualities = qualityLevels.map((quality) => { return { label: quality.label.toLowerCase(), name: quality.value }; }); return qualities; }; /** * Register event listeners on the given Echo player. * * @private * @param {HTMLElement} player */ const registerEchoPlayerEventListeneners = (player) => { player.resolveLoading = null; player.loadingPromise = new Promise((resolve) => { player.resolveLoading = resolve; }); player.onload = async () => { clearTimeout(loadingFailedTimeout); player.loadingPromise.then(async () => { this.trigger('ready'); this.trigger('loaded'); this.loadingComplete = true; this.trigger('resize'); if (trackOptions.length) { this.trigger('captions', trackOptions); } const autoplayIsAllowed = !window.H5PEditor && await H5P.Video.isAutoplayAllowed(); if (options.autoplay && autoplayIsAllowed) { this.play(); } return true; }); }; window.addEventListener('message', (event) => { let message = ''; try { message = JSON.parse(event.data); } catch (e) { return; } if ( message.context !== 'Echo360' || message.instanceId !== instanceId ) { return; } if (message.event === 'init') { // Set ratio if width and height is received from Echo360 if (message.data.width && message.data.height) { // If controls are displayed we have to add a magic height to make it visible :( ratio = ((message.data.height + (options.controls ? CONTROLS_HEIGHT : 0)) / message.data.width); } duration = message.data.duration; this.setCurrentTime(message.data.currentTime ?? 0); textTracks = message.data.textTracks ?? []; if (message.data.captions) { trackOptions = textTracks.map((track) => new H5P.Video.LabelValue(track.label, track.value) ); } player.resolveLoading(); // Player sends `init` event after rebuffering, unfortunately. if (!this.wasInitialized) { qualities = mapQualityLevels(message.data.qualityOptions); currentQuality = qualities[0].name; this.trigger('qualityChange', currentQuality); } this.trigger('resize'); if (message.data.playing) { changeState(H5P.Video.PLAYING); } this.wasInitialized = true; } else if (message.event === 'timeline') { updateUncertaintyCompensation(); duration = message.data.duration ?? this.getDuration(); if (timelineUpdatesToSkip === 0) { this.setCurrentTime(message.data.currentTime ?? 0); } else { timelineUpdatesToSkip--; } /* * Should work, but it was better if the player itself clearly sent * the state (playing, paused, ended) instead of us having to infer. */ const compensatedTime = this.getCurrentTime() + echoUncertaintyCompensationS * this.getPlaybackRate() if ( currentState === H5P.Video.PLAYING && Math.ceil(compensatedTime) >= duration ) { changeState(H5P.Video.ENDED); if (options.loop) { this.seek(0); this.play(); } return; } if (message.data.playing) { timeUpdate(currentTime); changeState(H5P.Video.PLAYING); } else if (currentState === H5P.Video.PLAYING) { // Condition prevents video to be paused on startup changeState(H5P.Video.PAUSED); window.clearTimeout(timeUpdateTimeout); } } }); }; /** * Update the uncertainty compensation value. * Computes the delta time between the last two timeline events sent by the * Echo360 player and updates the compensation value. */ const updateUncertaintyCompensation = () => { if (currentState === H5P.Video.PLAYING) { const time = Date.now(); if (previousTickMS) { echoUncertaintyCompensationS = Math.max( echoMinUncertaintyCompensationS, (time - previousTickMS + timelineUpdateDeltaSlackMS) / 1000 ) } else { echoUncertaintyCompensationS = echoMinUncertaintyCompensationS; } previousTickMS = time; } else { delete previousTickMS; } } /** * Change state of the player. * @param {number} state State id (H5P.Video[statename]). */ const changeState = (state) => { if (state !== currentState) { currentState = state; this.trigger('stateChange', state); } }; /** * Determine if the element is visible by computing the styles. * * @private * @param {HTMLElement} node - the element to check. * @returns {Boolean} true if it is visible. */ const isNodeVisible = (node) => { let style = window.getComputedStyle(node); if (node.offsetWidth === 0) { return false; } return ((style.display !== 'none') && (style.visibility !== 'hidden')); }; const timeUpdate = (time) => { window.clearTimeout(timeUpdateTimeout); this.lastTimeUpdate = Date.now(); timeUpdateTimeout = window.setTimeout(() => { if (currentState !== H5P.Video.PLAYING) { return; } const delta = (Date.now() - this.lastTimeUpdate) * this.getPlaybackRate(); this.setCurrentTime(currentTime + delta / 1000); timeUpdate(currentTime); }, 40); // 25 fps } /** * Create a new player by embedding an iframe. * * @private * @returns {Promise} */ const createEchoPlayer = async () => { if (!isNodeVisible(placeholderElement) || player !== undefined) { return; } // Since the SDK is loaded asynchronously below, explicitly set player to // null (unlike undefined) which indicates that creation has begun. This // allows the guard statement above to be hit if this function is called // more than once. player = null; let queryString = '?'; queryString += `instanceId=${instanceId}&`; if (options.controls) { queryString += 'controls=true&'; } if (options.disableFullscreen) { queryString += 'disableFullscreen=true&'; } if (options.deactivateSound) { queryString += 'deactivateSound=true&'; } if (options.startAt) { queryString += `startTimeMillis=${Math.round(options.startAt * 1000)}&`; } wrapperElement.innerHTML = `<iframe src="${sources[0].path}${queryString}" style="display: inline-block; width: 100%; height: 100%;" allow="autoplay; fullscreen" frameborder="0" scrolling="no"></iframe>`; player = wrapperElement.firstChild; // Create a new player registerEchoPlayerEventListeneners(player); loadingFailedTimeout = setTimeout(() => { failedLoading = true; removeLoadingIndicator(); wrapperElement.innerHTML = `<p class="echo-failed-loading">${l10n.unknownError}</p>`; wrapperElement.style.cssText = 'width: null; height: null;'; this.trigger('resize'); this.trigger('error', l10n.unknownError); }, LOADING_TIMEOUT_IN_SECONDS * 1000); }; /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ this.appendTo = ($container) => { $container.addClass('h5p-echo').append(wrapperElement); createEchoPlayer(); }; /** * Determine if the video has loaded. * * @public * @returns {Boolean} */ this.isLoaded = () => { return loadingComplete; }; /** * Get list of available qualities. * * @public * @returns {Array} */ this.getQualities = () => { return qualities; }; /** * Get the current quality. * * @public * @returns {String} Current quality identifier */ this.getQuality = () => { return currentQuality; }; /** * Set the playback quality. * * @public * @param {String} quality */ this.setQuality = async (quality) => { this.post('quality', quality); currentQuality = quality; this.trigger('qualityChange', currentQuality); }; /** * Start the video. * * @public */ this.play = () => { if (!player) { this.on('ready', this.play); return; } this.post('play', 0); }; /** * Pause the video. * * @public */ this.pause = () => { // Compensate for Echo360's delayed time updates timelineUpdatesToSkip = 1; this.post('pause', 0); }; /** * Seek video to given time. * * @public * @param {Number} time */ this.seek = (time) => { this.post('seek', time); this.setCurrentTime(time); // Compensate for Echo360's delayed time updates timelineUpdatesToSkip = 1; }; /** * Post a window message to the iframe. * * @public * @param event * @param data */ this.post = (event, data) => { player?.contentWindow?.postMessage( JSON.stringify({ event: event, context: 'Echo360', instanceId: instanceId, data: data }), '*' ); }; /** * Return the current play position. * * @public * @returns {Number} Seconds elapsed since beginning of video */ this.getCurrentTime = () => { return currentTime; }; /** * Set current time. * @param {number} timeS Time in seconds. */ this.setCurrentTime = (timeS) => { currentTime = timeS; } /** * Return the video duration. * * @public * @returns {?Number} Video duration in seconds */ this.getDuration = () => { if (duration > 0) { return duration; } return null; }; /** * Get percentage of video that is buffered. * * @public * @returns {Number} Between 0 and 100 */ this.getBuffered = () => { return buffered; }; /** * Mute the video. * * @public */ this.mute = () => { this.post('mute', 0); isMuted = true; }; /** * Unmute the video. * * @public */ this.unMute = () => { this.post('unmute', 0); isMuted = false; }; /** * Whether the video is muted. * * @public * @returns {Boolean} True if the video is muted, false otherwise */ this.isMuted = () => { return isMuted; }; /** * Get the video player's current sound volume. * * @public * @returns {Number} Between 0 and 100. */ this.getVolume = () => { return volume; }; /** * Set the video player's sound volume. * * @public * @param {Number} level */ this.setVolume = (level) => { this.post('volume', level); volume = level; }; /** * Get list of available playback rates. * * @public * @returns {Array} Available playback rates */ this.getPlaybackRates = () => { return [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; }; /** * Get the current playback rate. * * @public * @returns {Number} e.g. 0.5, 1, 1.5 or 2 */ this.getPlaybackRate = () => { return playbackRate; }; /** * Set the current playback rate. * * @public * @param {Number} rate Must be one of available rates from getPlaybackRates */ this.setPlaybackRate = async (rate) => { const echoRate = parseFloat(rate); this.post('playbackrate', echoRate); playbackRate = rate; this.trigger('playbackRateChange', rate); }; /** * Set current captions track. * * @public * @param {H5P.Video.LabelValue} track Captions to display */ this.setCaptionsTrack = (track) => { const echoCaption = trackOptions.find( (trackItem) => track?.value === trackItem.value ); trackOptions.forEach(trackItem => { trackItem.mode = (trackItem === echoCaption) ? 'showing' : 'disabled'; }); this.post('captions', echoCaption ? echoCaption.value : 'off'); }; /** * Get current captions track. * * @public * @returns {H5P.Video.LabelValue|null} Current captions track. */ this.getCaptionsTrack = () => { return trackOptions.find( (trackItem) => trackItem.mode === 'showing' ) ?? null; }; this.on('resize', () => { if (failedLoading || !isNodeVisible(wrapperElement)) { return; } if (player === undefined) { // Player isn't created yet. Try again. createEchoPlayer(); return; } // Use as much space as possible wrapperElement.style.cssText = 'width: 100%; height: 100%;'; const width = wrapperElement.clientWidth; const height = options.fit ? wrapperElement.clientHeight : (width * (ratio)); // Validate height before setting if (height > 0) { // Set size wrapperElement.style.cssText = 'width: ' + width + 'px; height: ' + height + 'px;'; } }); } /** * Find id of video from given URL. * * @private * @param {String} url * @returns {String} Echo video identifier */ const getId = (url) => { const matches = url.match(/^[^/]+:\/\/(echo360[^/]+)\/media\/([^/]+)\/h5p.*$/i); if (matches && matches.length === 3) { return [matches[2], matches[2]]; } }; /** * Check to see if we can play any of the given sources. * * @public * @static * @param {Array} sources * @returns {Boolean} */ EchoPlayer.canPlay = (sources) => { return getId(sources[0].path); }; return EchoPlayer; })(H5P.jQuery); // Register video handler H5P.videoHandlers = H5P.videoHandlers || []; H5P.videoHandlers.push(H5P.VideoEchoVideo); ; /** @namespace H5P */ H5P.VideoHtml5 = (function ($) { /** * HTML5 video player for H5P. * * @class * @param {Array} sources Video files to use * @param {Object} options Settings for the player * @param {Object} l10n Localization strings */ function Html5(sources, options, l10n) { var self = this; /** * Small helper to ensure all video sources get the same cache buster. * * @private * @param {Object} source * @return {string} */ const getCrossOriginPath = function (source) { let path = H5P.getPath(source.path, self.contentId); if (video.crossOrigin !== null && H5P.addQueryParameter && H5PIntegration.crossoriginCacheBuster) { path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster); } return path }; /** * Register track to video * * @param {Object} trackData Track object * @param {string} trackData.kind Kind of track * @param {Object} trackData.track Source path * @param {string} [trackData.label] Label of track * @param {string} [trackData.srcLang] Language code */ const addTrack = function (trackData) { // Skip invalid tracks if (!trackData.kind || !trackData.track.path) { return; } var track = document.createElement('track'); track.kind = trackData.kind; track.src = getCrossOriginPath(trackData.track); // Uses same crossOrigin as parent. You cannot mix. if (trackData.label) { track.label = trackData.label; } if (trackData.srcLang) { track.srcLang = trackData.srcLang; } return track; }; /** * Small helper to set the inital video source. * Useful if some of the loading happens asynchronously. * NOTE: Setting the crossOrigin must happen before any of the * sources(poster, tracks etc.) are loaded * * @private */ const setInitialSource = function () { if (qualities[currentQuality] === undefined) { return; } if (H5P.setSource !== undefined) { H5P.setSource(video, qualities[currentQuality].source, self.contentId) } else { // Backwards compatibility (H5P < v1.22) const srcPath = H5P.getPath(qualities[currentQuality].source.path, self.contentId); if (H5P.getCrossOrigin !== undefined) { var crossOrigin = H5P.getCrossOrigin(srcPath); video.setAttribute('crossorigin', crossOrigin !== null ? crossOrigin : 'anonymous'); } video.src = srcPath; } // Add poster if provided if (options.poster) { video.poster = getCrossOriginPath(options.poster); // Uses same crossOrigin as parent. You cannot mix. } // Register tracks options.tracks.forEach(function (track, i) { var trackElement = addTrack(track); if (i === 0) { trackElement.default = true; } if (trackElement) { video.appendChild(trackElement); } }); }; /** * Displayed when the video is buffering * @private */ var $throbber = $('<div/>', { 'class': 'h5p-video-loading' }); /** * Used to display error messages * @private */ var $error = $('<div/>', { 'class': 'h5p-video-error' }); /** * Keep track of current state when changing quality. * @private */ var stateBeforeChangingQuality; var currentTimeBeforeChangingQuality; /** * Avoids firing the same event twice. * @private */ var lastState; /** * Keeps track whether or not the video has been loaded. * @private */ var isLoaded = false; /** * * @private */ var playbackRate = 1; var skipRateChange = false; // Create player var video = document.createElement('video'); // Sort sources into qualities var qualities = getQualities(sources, video); var currentQuality; numQualities = 0; for (let quality in qualities) { numQualities++; } if (numQualities > 1 && H5P.VideoHtml5.getExternalQuality !== undefined) { H5P.VideoHtml5.getExternalQuality(sources, function (chosenQuality) { if (qualities[chosenQuality] !== undefined) { currentQuality = chosenQuality; } setInitialSource(); }); } else { // Select quality and source currentQuality = getPreferredQuality(); if (currentQuality === undefined || qualities[currentQuality] === undefined) { // No preferred quality, pick the first. for (currentQuality in qualities) { if (qualities.hasOwnProperty(currentQuality)) { break; } } } setInitialSource(); } // Setting webkit-playsinline, which makes iOS 10 beeing able to play video // inside browser. video.setAttribute('webkit-playsinline', ''); video.setAttribute('playsinline', ''); video.setAttribute('preload', 'metadata'); // Remove buttons in Chrome's video player: let controlsList = 'nodownload'; if (options.disableFullscreen) { controlsList += ' nofullscreen'; } if (options.disableRemotePlayback) { controlsList += ' noremoteplayback'; } video.setAttribute('controlsList', controlsList); // Remove picture in picture as it interfers with other video players video.disablePictureInPicture = true; // Set options video.disableRemotePlayback = (options.disableRemotePlayback ? true : false); video.controls = (options.controls ? true : false); // Hardcoded autoplay to false to avoid playing videos on init video.autoplay = false; video.loop = (options.loop ? true : false); video.className = 'h5p-video'; video.style.display = 'block'; if (options.fit) { // Style is used since attributes with relative sizes aren't supported by IE9. video.style.width = '100%'; video.style.height = '100%'; } /** * Helps registering events. * * @private * @param {String} native Event name * @param {String} h5p Event name * @param {String} [arg] Optional argument */ var mapEvent = function (native, h5p, arg) { video.addEventListener(native, function () { switch (h5p) { case 'loaded': isLoaded = true; if (stateBeforeChangingQuality !== undefined) { return; // Avoid loaded event when changing quality. } // Remove any errors if ($error.is(':visible')) { $error.remove(); } if (OLD_ANDROID_FIX) { var andLoaded = function () { video.removeEventListener('durationchange', andLoaded, false); // On Android seeking isn't ready until after play. self.trigger(h5p); }; video.addEventListener('durationchange', andLoaded, false); return; } if (video.poster) { $(video).one('play', function () { self.seek(self.getCurrentTime() || options.startAt); }); } else { self.seek(options.startAt); } break; case 'error': // Handle error and get message. arg = error(arguments[0], arguments[1]); break; case 'playbackRateChange': // Fix for keeping playback rate in IE11 if (skipRateChange) { skipRateChange = false; return; // Avoid firing event when changing back } if (H5P.Video.IE11_PLAYBACK_RATE_FIX && playbackRate != video.playbackRate) { // Intentional // Prevent change in playback rate not triggered by the user video.playbackRate = playbackRate; skipRateChange = true; return; } // End IE11 fix arg = self.getPlaybackRate(); break; } self.trigger(h5p, arg); }, false); }; /** * Handle errors from the video player. * * @private * @param {Object} code Error * @param {String} [message] * @returns {String} Human readable error message. */ var error = function (code, message) { if (code instanceof Event) { // No error code if (!code.target.error) { return ''; } switch (code.target.error.code) { case MediaError.MEDIA_ERR_ABORTED: message = l10n.aborted; break; case MediaError.MEDIA_ERR_NETWORK: message = l10n.networkFailure; break; case MediaError.MEDIA_ERR_DECODE: message = l10n.cannotDecode; break; case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: message = l10n.formatNotSupported; break; case MediaError.MEDIA_ERR_ENCRYPTED: message = l10n.mediaEncrypted; break; } } if (!message) { message = l10n.unknownError; } // Hide throbber $throbber.remove(); // Display error message to user $error.text(message).insertAfter(video); // Pass message to our error event return message; }; /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ self.appendTo = function ($container) { $container.append(video); }; /** * Get list of available qualities. Not available until after play. * * @public * @returns {Array} */ self.getQualities = function () { // Create reverse list var options = []; for (var q in qualities) { if (qualities.hasOwnProperty(q)) { options.splice(0, 0, { name: q, label: qualities[q].label }); } } if (options.length < 2) { // Do not return if only one quality. return; } return options; }; /** * Get current playback quality. Not available until after play. * * @public * @returns {String} */ self.getQuality = function () { return currentQuality; }; /** * Set current playback quality. Not available until after play. * Listen to event "qualityChange" to check if successful. * * @public * @params {String} [quality] */ self.setQuality = function (quality) { if (qualities[quality] === undefined || quality === currentQuality) { return; // Invalid quality } // Keep track of last choice setPreferredQuality(quality); // Avoid multiple loaded events if changing quality multiple times. if (!stateBeforeChangingQuality) { // Keep track of last state stateBeforeChangingQuality = lastState; // Keep track of current time currentTimeBeforeChangingQuality = video.currentTime; // Seek and start video again after loading. var loaded = function () { video.removeEventListener('loadedmetadata', loaded, false); if (OLD_ANDROID_FIX) { var andLoaded = function () { video.removeEventListener('durationchange', andLoaded, false); // On Android seeking isn't ready until after play. self.seek(currentTimeBeforeChangingQuality); }; video.addEventListener('durationchange', andLoaded, false); } else { // Seek to current time. self.seek(currentTimeBeforeChangingQuality); } // Always play to get image. video.play(); if (stateBeforeChangingQuality !== H5P.Video.PLAYING) { // Do not resume playing video.pause(); } // Done changing quality stateBeforeChangingQuality = undefined; // Remove any errors if ($error.is(':visible')) { $error.remove(); } }; video.addEventListener('loadedmetadata', loaded, false); } // Keep track of current quality currentQuality = quality; self.trigger('qualityChange', currentQuality); // Display throbber self.trigger('stateChange', H5P.Video.BUFFERING); // Change source video.src = getCrossOriginPath(qualities[quality].source); // (iPad does not support #t=). // Note: Optional tracks use same crossOrigin as the original. You cannot mix. // Remove poster so it will not show during quality change video.removeAttribute('poster'); }; /** * Starts the video. * * @public * @return {Promise|undefined} May return a Promise that resolves when * play has been processed. */ self.play = function () { if ($error.is(':visible')) { return; } if (!isLoaded) { // Make sure video is loaded before playing video.load(); video.addEventListener('loadeddata', function() { video.play(); }, false); } else { return video.play(); } }; /** * Pauses the video. * * @public */ self.pause = function () { video.pause(); }; /** * Seek video to given time. * * @public * @param {Number} time */ self.seek = function (time) { video.currentTime = time; // Use canplaythrough for IOs devices // Use loadedmetadata for all other devices. const eventName = navigator.userAgent.match(/iPad|iPod|iPhone/i) ? "canplaythrough" : "loadedmetadata"; function seekTo() { video.currentTime = time; video.removeEventListener(eventName, seekTo); }; if (video.readyState === 4) { seekTo(); } else { video.addEventListener(eventName, seekTo); } }; /** * Get elapsed time since video beginning. * * @public * @returns {Number} */ self.getCurrentTime = function () { return video.currentTime; }; /** * Get total video duration time. * * @public * @returns {Number} */ self.getDuration = function () { if (isNaN(video.duration)) { return; } return video.duration; }; /** * Get percentage of video that is buffered. * * @public * @returns {Number} Between 0 and 100 */ self.getBuffered = function () { // Find buffer currently playing from var buffered = 0; for (var i = 0; i < video.buffered.length; i++) { var from = video.buffered.start(i); var to = video.buffered.end(i); if (video.currentTime > from && video.currentTime < to) { buffered = to; break; } } // To percentage return buffered ? (buffered / video.duration) * 100 : 0; }; /** * Turn off video sound. * * @public */ self.mute = function () { video.muted = true; }; /** * Turn on video sound. * * @public */ self.unMute = function () { video.muted = false; }; /** * Check if video sound is turned on or off. * * @public * @returns {Boolean} */ self.isMuted = function () { return video.muted; }; /** * Returns the video sound level. * * @public * @returns {Number} Between 0 and 100. */ self.getVolume = function () { return video.volume * 100; }; /** * Set video sound level. * * @public * @param {Number} level Between 0 and 100. */ self.setVolume = function (level) { video.volume = level / 100; }; /** * Get list of available playback rates. * * @public * @returns {Array} available playback rates */ self.getPlaybackRates = function () { /* * not sure if there's a common rule about determining good speeds * using Google's standard options via a constant for setting */ var playbackRates = PLAYBACK_RATES; return playbackRates; }; /** * Get current playback rate. * * @public * @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2 */ self.getPlaybackRate = function () { return video.playbackRate; }; /** * Set current playback rate. * Listen to event "playbackRateChange" to check if successful. * * @public * @params {Number} suggested rate that may be rounded to supported values */ self.setPlaybackRate = function (newPlaybackRate) { playbackRate = newPlaybackRate; video.playbackRate = newPlaybackRate; }; /** * Set current captions track. * * @param {H5P.Video.LabelValue} Captions track to show during playback */ self.setCaptionsTrack = function (track) { for (var i = 0; i < video.textTracks.length; i++) { video.textTracks[i].mode = (track && track.value === i ? 'showing' : 'disabled'); } }; /** * Figure out which captions track is currently used. * * @return {H5P.Video.LabelValue} Captions track */ self.getCaptionsTrack = function () { for (var i = 0; i < video.textTracks.length; i++) { if (video.textTracks[i].mode === 'showing') { return new H5P.Video.LabelValue(video.textTracks[i].label, i); } } return null; }; // Register event listeners mapEvent('ended', 'stateChange', H5P.Video.ENDED); mapEvent('playing', 'stateChange', H5P.Video.PLAYING); mapEvent('pause', 'stateChange', H5P.Video.PAUSED); mapEvent('waiting', 'stateChange', H5P.Video.BUFFERING); mapEvent('loadedmetadata', 'loaded'); mapEvent('canplay', 'canplay'); mapEvent('error', 'error'); mapEvent('ratechange', 'playbackRateChange'); if (!video.controls) { // Disable context menu(right click) to prevent controls. video.addEventListener('contextmenu', function (event) { event.preventDefault(); }, false); } // Display throbber when buffering/loading video. self.on('stateChange', function (event) { var state = event.data; lastState = state; if (state === H5P.Video.BUFFERING) { $throbber.insertAfter(video); } else { $throbber.remove(); } }); // Load captions after the video is loaded self.on('loaded', function () { nextTick(function () { var textTracks = []; for (var i = 0; i < video.textTracks.length; i++) { textTracks.push(new H5P.Video.LabelValue(video.textTracks[i].label, i)); } if (textTracks.length) { self.trigger('captions', textTracks); } }); }); // Alternative to 'canplay' event /*self.on('resize', function () { if (video.offsetParent === null) { return; } video.style.width = '100%'; video.style.height = '100%'; var width = video.clientWidth; var height = options.fit ? video.clientHeight : (width * (video.videoHeight / video.videoWidth)); video.style.width = width + 'px'; video.style.height = height + 'px'; });*/ // Video controls are ready nextTick(function () { self.trigger('ready'); }); /** * Check video is loaded and ready play. * * @public * @param {Boolean} */ self.isLoaded = function () { return isLoaded; }; } /** * Check to see if we can play any of the given sources. * * @public * @static * @param {Array} sources * @returns {Boolean} */ Html5.canPlay = function (sources) { var video = document.createElement('video'); if (video.canPlayType === undefined) { return false; // Not supported } // Cycle through sources for (var i = 0; i < sources.length; i++) { var type = getType(sources[i]); if (type && video.canPlayType(type) !== '') { // We should be able to play this return true; } } return false; }; /** * Find source type. * * @private * @param {Object} source * @returns {String} */ var getType = function (source) { var type = source.mime; if (!type) { // Try to get type from URL var matches = source.path.match(/\.(\w+)$/); if (matches && matches[1]) { type = 'video/' + matches[1]; } } if (type && source.codecs) { // Add codecs type += '; codecs="' + source.codecs + '"'; } return type; }; /** * Sort sources into qualities. * * @private * @static * @param {Array} sources * @param {Object} video * @returns {Object} Quality mapping */ var getQualities = function (sources, video) { var qualities = {}; var qualityIndex = 1; var lastQuality; // Cycle through sources for (var i = 0; i < sources.length; i++) { var source = sources[i]; // Find and update type. var type = source.type = getType(source); // Check if we support this type var isPlayable = type && (type === 'video/unknown' || video.canPlayType(type) !== ''); if (!isPlayable) { continue; // We cannot play this source } if (source.quality === undefined) { /** * No quality metadata. Create a quality tag to separate multiple sources of the same type, * e.g. if two mp4 files with different quality has been uploaded */ if (lastQuality === undefined || qualities[lastQuality].source.type === type) { // Create a new quality tag source.quality = { name: 'q' + qualityIndex, label: (source.metadata && source.metadata.qualityName) ? source.metadata.qualityName : 'Quality ' + qualityIndex // TODO: l10n }; qualityIndex++; } else { /** * Assumes quality already exists in a different format. * Uses existing label for this quality. */ source.quality = qualities[lastQuality].source.quality; } } // Log last quality lastQuality = source.quality.name; // Look to see if quality exists var quality = qualities[lastQuality]; if (quality) { // We have a source with this quality. Check if we have a better format. if (source.mime.split('/')[1] === PREFERRED_FORMAT) { quality.source = source; } } else { // Add new source with quality. qualities[source.quality.name] = { label: source.quality.label, source: source }; } } return qualities; }; /** * Set preferred video quality. * * @private * @static * @param {String} quality Index of preferred quality */ var setPreferredQuality = function (quality) { try { localStorage.setItem('h5pVideoQuality', quality); } catch (err) { console.warn('Unable to set preferred video quality, localStorage is not available.'); } }; /** * Get preferred video quality. * * @private * @static * @returns {String} Index of preferred quality */ var getPreferredQuality = function () { // First check localStorage let quality; try { quality = localStorage.getItem('h5pVideoQuality'); } catch (err) { console.warn('Unable to retrieve preferred video quality from localStorage.'); } if (!quality) { try { // The fallback to old cookie solution var settings = document.cookie.split(';'); for (var i = 0; i < settings.length; i++) { var setting = settings[i].split('='); if (setting[0] === 'H5PVideoQuality') { quality = setting[1]; break; } } } catch (err) { console.warn('Unable to retrieve preferred video quality from cookie.'); } } return quality; }; /** * Helps schedule a task for the next tick. * @param {function} task */ var nextTick = function (task) { setTimeout(task, 0); }; /** @constant {Boolean} */ var OLD_ANDROID_FIX = false; /** @constant {Boolean} */ var PREFERRED_FORMAT = 'mp4'; /** @constant {Object} */ var PLAYBACK_RATES = [0.25, 0.5, 1, 1.25, 1.5, 2]; if (navigator.userAgent.indexOf('Android') !== -1) { // We have Android, check version. var version = navigator.userAgent.match(/AppleWebKit\/(\d+\.?\d*)/); if (version && version[1] && Number(version[1]) <= 534.30) { // Include fix for devices running the native Android browser. // (We don't know when video was fixed, so the number is just the lastest // native android browser we found.) OLD_ANDROID_FIX = true; } } else { if (navigator.userAgent.indexOf('Chrome') !== -1) { // If we're using chrome on a device that isn't Android, prefer the webm // format. This is because Chrome has trouble with some mp4 codecs. PREFERRED_FORMAT = 'webm'; } } return Html5; })(H5P.jQuery); // Register video handler H5P.videoHandlers = H5P.videoHandlers || []; H5P.videoHandlers.push(H5P.VideoHtml5); ; /** @namespace H5P */ H5P.Video = (function ($, ContentCopyrights, MediaCopyright, handlers) { /** * The ultimate H5P video player! * * @class * @param {Object} parameters Options for this library. * @param {Object} parameters.visuals Visual options * @param {Object} parameters.playback Playback options * @param {Object} parameters.a11y Accessibility options * @param {Boolean} [parameters.startAt] Start time of video * @param {Number} id Content identifier * @param {Object} [extras] Extra parameters. */ function Video(parameters, id, extras = {}) { var self = this; self.oldTime = extras.previousState?.time; self.contentId = id; self.WAS_RESET = false; self.startAt = parameters.startAt || 0; // Ref youtube.js - ipad & youtube - issue self.pressToPlay = false; // Reference to the handler var handlerName = ''; // Initialize event inheritance H5P.EventDispatcher.call(self); // Default language localization parameters = $.extend(true, parameters, { l10n: { name: 'Video', loading: 'Video player loading...', noPlayers: 'Found no video players that supports the given video format.', noSources: 'Video source is missing.', aborted: 'Media playback has been aborted.', networkFailure: 'Network failure.', cannotDecode: 'Unable to decode media.', formatNotSupported: 'Video format not supported.', mediaEncrypted: 'Media encrypted.', unknownError: 'Unknown error.', vimeoPasswordError: 'Password-protected Vimeo videos are not supported.', vimeoPrivacyError: 'The Vimeo video cannot be used due to its privacy settings.', vimeoLoadingError: 'The Vimeo video could not be loaded.', invalidYtId: 'Invalid YouTube ID.', unknownYtId: 'Unable to find video with the given YouTube ID.', restrictedYt: 'The owner of this video does not allow it to be embedded.' } }); parameters.a11y = parameters.a11y || []; parameters.playback = parameters.playback || {}; parameters.visuals = $.extend( true, { disableFullscreen: false }, parameters.visuals ); /** @private */ var sources = []; if (parameters.sources) { for (var i = 0; i < parameters.sources.length; i++) { // Clone to avoid changing of parameters. var source = $.extend(true, {}, parameters.sources[i]); // Create working URL without html entities. source.path = $cleaner.html(source.path).text(); sources.push(source); } } /** @private */ var tracks = []; parameters.a11y.forEach(function (track) { // Clone to avoid changing of parameters. var clone = $.extend(true, {}, track); // Create working URL without html entities if (clone.track && clone.track.path) { clone.track.path = $cleaner.html(clone.track.path).text(); tracks.push(clone); } }); /** * Handle autoplay. If autoplay is disabled, it will still autopause when * video is not visible. * * @param {*} $container */ const handleAutoPlayPause = function ($container) { // Keep the current state let state; self.on('stateChange', function(event) { state = event.data; }); // Keep record of autopauses. // I.e: we don't wanna autoplay if the user has excplicitly paused. self.autoPaused = !self.pressToPlay; new IntersectionObserver(function (entries) { const entry = entries[0]; // This video element became visible if (entry.isIntersecting) { // Autoplay if autoplay is enabled and it was not explicitly // paused by a user if (parameters.playback.autoplay && self.autoPaused) { self.autoPaused = false; self.play(); } } else if (state !== Video.PAUSED && state !== Video.ENDED) { self.autoPaused = true; self.pause(); } }, { root: null, threshold: [0, 1] // Get events when it is shown and hidden }).observe($container.get(0)); }; /** * Attaches the video handler to the given container. * Inserts text if no handler is found. * * @public * @param {jQuery} $container */ self.attach = function ($container) { $container.addClass('h5p-video').html(''); if (self.appendTo !== undefined) { self.appendTo($container); // Avoid autoplaying in authoring tool if (window.H5PEditor === undefined) { handleAutoPlayPause($container); } } else if (sources.length) { $container.text(parameters.l10n.noPlayers); } else { $container.text(parameters.l10n.noSources); } }; /** * Get name of the video handler * * @public * @returns {string} */ self.getHandlerName = function() { return handlerName; }; /** * @public * Get current state for resume support. * * @returns {object} Current state. */ self.getCurrentState = function () { if (self.getCurrentTime) { return { time: self.getCurrentTime() || self.oldTime, }; } }; /** * The two functions below needs to be defined in this base class, * since it is used in this class even if no handler was found. */ self.seek = () => {}; self.pause = () => {}; /** * @public * Reset current state (time). * */ self.resetTask = function () { delete self.oldTime; self.resetPlayback(parameters.startAt || 0); }; /** * Default implementation of resetPlayback. May be overridden by sub classes. * * @param {*} startAt */ self.resetPlayback = startAt => { self.seek(startAt); self.pause(); self.WAS_RESET = true; }; // Resize the video when we know its aspect ratio self.on('loaded', function () { self.trigger('resize'); // reset time if wasn't done immediately if (self.WAS_RESET) { self.seek(parameters.startAt || 0); if (!parameters.playback.autoplay) { self.pause(); } self.WAS_RESET = false; } }); // Find player for video sources if (sources.length) { const options = { controls: parameters.visuals.controls, autoplay: parameters.playback.autoplay, loop: parameters.playback.loop, fit: parameters.visuals.fit, poster: parameters.visuals.poster === undefined ? undefined : parameters.visuals.poster, tracks: tracks, disableRemotePlayback: parameters.visuals.disableRemotePlayback === true, disableFullscreen: parameters.visuals.disableFullscreen === true, deactivateSound: parameters.playback.deactivateSound, } if (!self.WAS_RESET) { options.startAt = self.oldTime !== undefined ? self.oldTime : (parameters.startAt || 0); } var html5Handler; for (var i = 0; i < handlers.length; i++) { var handler = handlers[i]; if (handler.canPlay !== undefined && handler.canPlay(sources)) { handler.call(self, sources, options, parameters.l10n); handlerName = handler.name; return; } if (handler === H5P.VideoHtml5) { html5Handler = handler; handlerName = handler.name; } } // Fallback to trying HTML5 player if (html5Handler) { html5Handler.call(self, sources, options, parameters.l10n); } } } // Extends the event dispatcher Video.prototype = Object.create(H5P.EventDispatcher.prototype); Video.prototype.constructor = Video; // Player states /** @constant {Number} */ Video.ENDED = 0; /** @constant {Number} */ Video.PLAYING = 1; /** @constant {Number} */ Video.PAUSED = 2; /** @constant {Number} */ Video.BUFFERING = 3; /** * When video is queued to start * @constant {Number} */ Video.VIDEO_CUED = 5; // Used to convert between html and text, since URLs have html entities. var $cleaner = H5P.jQuery('<div/>'); /** * Help keep track of key value pairs used by the UI. * * @class * @param {string} label * @param {string} value */ Video.LabelValue = function (label, value) { this.label = label; this.value = value; }; /** * Determine whether video can be autoplayed. * @returns {Promise<boolean>} Whether autoplay is allowed. */ Video.isAutoplayAllowed = async () => { if (document.featurePolicy?.allowsFeature('autoplay')) { return true; // Browser supports `featurePolicy` and can tell directly } const video = document.createElement('video'); /* * Without a video source, the play Promise will be rejected with an error * if it cannot be autoplayed, but not resolve at all if it can be * autoplayed. Using a timeout to detect the latter case here. */ const timeoutMs = 50; // If play promise rejects, then within few ms const timeoutPromise = new Promise((resolve) => { window.setTimeout(() => { resolve(true); // Timeout reached, autoplay is allowed }, timeoutMs); }); let result; try { result = (await Promise.race([video.play(), timeoutPromise])) ?? true; } catch (error) { result = false; } return result; }; /** @constant {Boolean} */ Video.IE11_PLAYBACK_RATE_FIX = (navigator.userAgent.match(/Trident.*rv[ :]*11\./) ? true : false); return Video; })(H5P.jQuery, H5P.ContentCopyrights, H5P.MediaCopyright, H5P.videoHandlers || []); ; H5P = H5P || {}; /** * Will render a Question with multiple choices for answers. * * Events provided: * - h5pQuestionSetFinished: Triggered when a question is finished. (User presses Finish-button) * * @param {Array} options * @param {int} contentId * @param {Object} contentData * @returns {H5P.QuestionSet} Instance */ H5P.QuestionSet = function (options, contentId, contentData) { if (!(this instanceof H5P.QuestionSet)) { return new H5P.QuestionSet(options, contentId, contentData); } H5P.EventDispatcher.call(this); var $ = H5P.jQuery; var self = this; this.contentId = contentId; var defaults = { initialQuestion: 0, progressType: 'dots', passPercentage: 50, questions: [], introPage: { showIntroPage: false, title: '', introduction: '', startButtonText: 'Start' }, texts: { prevButton: 'Previous question', nextButton: 'Next question', finishButton: 'Finish', submitButton: 'Submit', textualProgress: 'Question: @current of @total questions', jumpToQuestion: 'Question %d of %total', questionLabel: 'Question', readSpeakerProgress: 'Question @current of @total', unansweredText: 'Unanswered', answeredText: 'Answered', currentQuestionText: 'Current question', navigationLabel: 'Questions', questionSetInstruction: 'Choose question to display' }, endGame: { showResultPage: true, noResultMessage: 'Finished', message: 'Your result:', scoreBarLabel: 'You got @finals out of @totals points', oldFeedback: { successGreeting: '', successComment: '', failGreeting: '', failComment: '' }, overallFeedback: [], finishButtonText: 'Finish', submitButtonText: 'Submit', solutionButtonText: 'Show solution', retryButtonText: 'Retry', showAnimations: false, skipButtonText: 'Skip video', showSolutionButton: true, showRetryButton: true }, override: {}, disableBackwardsNavigation: false }; this.isSubmitting = contentData && (contentData.isScoringEnabled || contentData.isReportingEnabled); var params = $.extend(true, {}, defaults, options); var initialParams = $.extend(true, {}, defaults, options); var poolOrder; // Order of questions in a pool var currentQuestion = 0; var questionInstances = []; var questionOrder; //Stores order of questions to allow resuming of question set var $myDom; var scoreBar; var up; var renderSolutions = false; var showingSolutions = false; contentData = contentData || {}; // Need to check with isEmpty, as {} == true this.hasPrevState = !H5P.isEmpty(contentData.previousState); // Bring question set up to date when resuming if (self.hasPrevState) { if (contentData.previousState.progress !== undefined) { currentQuestion = contentData.previousState.progress; } questionOrder = contentData.previousState.order; } /** * Randomizes questions in an array and updates an array containing their order * @param {array} questions * @return {Object.<array, array>} questionOrdering */ var randomizeQuestionOrdering = function (questions) { // Use a previous order if it exists if (self.hasPrevState && contentData.previousState.order) { questions = questions.slice(0, questionOrder.length); return { questions: questions, questionOrder: questionOrder, } } // Save the original order of the questions in a multidimensional array [[question0,0],[question1,1]... var questionOrdering = questions.map(function (questionInstance, index) { return [questionInstance, index]; }); // Shuffle the multidimensional array questionOrdering = H5P.shuffleArray(questionOrdering); // Retrieve question objects from the first index questions = []; for (var i = 0; i < questionOrdering.length; i++) { questions[i] = questionOrdering[i][0]; } // Retrieve the new shuffled order from the second index var newOrder = []; for (var j = 0; j < questionOrdering.length; j++) { newOrder[j] = questionOrdering[j][1]; } // Return the questions in their new order *with* their new indexes return { questions: questions, questionOrder: newOrder }; }; // Create a pool (a subset) of questions if necessary if (params.poolSize > 0) { // If a previous pool exists, recreate it if (self.hasPrevState && contentData.previousState.poolOrder) { poolOrder = contentData.previousState.poolOrder; // Recreate the pool from the saved data var pool = []; for (var i = 0; i < poolOrder.length; i++) { pool[i] = params.questions[poolOrder[i]]; } // Replace original questions with just the ones in the pool params.questions = pool; } else { // Otherwise create a new pool // Randomize and get the results var poolResult = randomizeQuestionOrdering(params.questions); var poolQuestions = poolResult.questions; poolOrder = poolResult.questionOrder; // Discard extra questions poolQuestions = poolQuestions.slice(0, params.poolSize); poolOrder = poolOrder.slice(0, params.poolSize); // Replace original questions with just the ones in the pool params.questions = poolQuestions; } } // Set overrides for questions var override; if (params.override.showSolutionButton || params.override.retryButton || params.override.checkButton === false) { override = {}; if (params.override.showSolutionButton) { // Force "Show solution" button to be on or off for all interactions override.enableSolutionsButton = (params.override.showSolutionButton === 'on' ? true : false); } if (params.override.retryButton) { // Force "Retry" button to be on or off for all interactions override.enableRetry = (params.override.retryButton === 'on' ? true : false); } if (params.override.checkButton === false) { // Force "Check" button to be on or off for all interactions override.enableCheckButton = params.override.checkButton; } } /** * Generates question instances from H5P objects * * @param {object} questions H5P content types to be created as instances * @return {array} Array of questions instances */ var createQuestionInstancesFromQuestions = function (questions) { var result = []; // Create question instances from questions // Instantiate question instances for (var i = 0; i < questions.length; i++) { var question; // If a previous order exists, use it if (questionOrder !== undefined) { question = questions[questionOrder[i]]; } else { // Use a generic order when initialzing for the first time question = questions[i]; } if (override) { // Extend subcontent with the overrided settings. $.extend(question.params.behaviour, override); } question.params = question.params || {}; var hasAnswers = self.hasPrevState && contentData.previousState.answers; var questionInstance = H5P.newRunnable(question, contentId, undefined, undefined, { previousState: hasAnswers ? contentData.previousState.answers[i] : undefined, parent: self }); questionInstance.on('resize', function () { up = true; self.trigger('resize'); }); result.push(questionInstance); } return result; }; // Create question instances from questions given by params questionInstances = createQuestionInstancesFromQuestions(params.questions); params.noOfQuestionAnswered = 0; if (self.hasPrevState) { // get numbers of questions answered by user if (contentData.previousState.answers) { for (var i = 0; i < questionInstances.length; i++) { let answered = questionInstances[i].getAnswerGiven(); if (answered){ params.noOfQuestionAnswered++; } } } } // Create html for intro page layout self.$introPage = ''; if (params.introPage.showIntroPage && params.noOfQuestionAnswered === 0) { self.$introPage = $('<div>', { class: 'intro-page' }); if (params.introPage.title) { $('<div>', { class: 'title', html: '<h1>' + params.introPage.title + '</h1>', appendTo: self.$introPage }); } if (params.introPage.introduction) { $('<div>', { class: 'introduction', html: params.introPage.introduction, appendTo: self.$introPage }); } self.$introButtonsContainer = $('<div>', { class: 'buttons', appendTo: self.$introPage }); $('<button>', { class: 'qs-startbutton h5p-joubelui-button h5p-button', html: params.introPage.startButtonText, appendTo: self.$introButtonsContainer }); } // Create html for progress announcer self.$progressAnnouncer = $('<div>', { class: 'qs-progress-announcer', 'aria-live': 'polite', }); // Create html for questionset self.$questionsContainer = $('<div>', { class: 'questionset ' + ((params.introPage.showIntroPage && params.noOfQuestionAnswered === 0) ? 'hidden' : ''), }); const tabIDs = Array.from({length: params.questions.length}, () => H5P.createUUID()); const tabPanelIDs = Array.from({length: params.questions.length}, () => H5P.createUUID()); for (let i=0; i<params.questions.length; i++) { $('<div>', { class: 'question-container', role: 'tabpanel', id: tabPanelIDs[i], 'aria-labelledby': tabIDs[i], appendTo: self.$questionsContainer }); } self.$footer = $('<div>', { class: 'qs-footer', appendTo: self.$questionsContainer }); self.$progressBar = $('<div>', { class: 'qs-progress', role: 'navigation', 'aria-label': params.texts.navigationLabel, appendTo: self.$footer }); if (params.progressType == "dots") { self.$dotsContainer = $('<ul>', { class: 'dots-container', role: 'tablist', 'aria-label': params.texts.questionSetInstruction, appendTo: self.$progressBar }); for (let i=0; i<params.questions.length; i++) { $('<li>', { class: 'progress-item', html: '<a href="#" class= "progress-dot unanswered ' + (params.disableBackwardsNavigation ? 'disabled' : '') + '" ' + 'id="' + tabIDs[i] + '" ' + 'aria-label=' + '"' + params.texts.jumpToQuestion.replace("%d", i + 1).replace("%total", params.questions.length) + ', ' + params.texts.unansweredText + '" ' + 'tabindex="-1" ' + (params.disableBackwardsNavigation ? 'aria-disabled="true"' : '') + ' aria-controls="' + tabPanelIDs[i] + '" ' + 'aria-selected="false" ' + 'role="tab"></a>', appendTo: self.$dotsContainer }) } } else if (params.progressType == "textual") { $('<span>', { class: 'progress-text', appendTo: self.$progressBar }) } // Randomize questions only on instantiation if (params.randomQuestions && !self.hasPrevState) { var result = randomizeQuestionOrdering(questionInstances); questionInstances = result.questions; questionOrder = result.questionOrder; } // Resize all interactions on resize self.on('resize', function () { if (up) { // Prevent resizing the question again. up = false; return; } for (var i = 0; i < questionInstances.length; i++) { questionInstances[i].trigger('resize'); } }); // Update button state. var _updateButtons = function () { // Verify that current question is answered when backward nav is disabled if (params.disableBackwardsNavigation) { if (questionInstances[currentQuestion].getAnswerGiven() && questionInstances.length-1 !== currentQuestion) { questionInstances[currentQuestion].showButton('next'); } else { questionInstances[currentQuestion].hideButton('next'); } } var answered = true; for (var i = questionInstances.length - 1; i >= 0; i--) { answered = answered && (questionInstances[i]).getAnswerGiven(); } if (currentQuestion === (params.questions.length - 1) && questionInstances[currentQuestion]) { if (answered) { questionInstances[currentQuestion].showButton('finish'); } else { questionInstances[currentQuestion].hideButton('finish'); } } }; var _showQuestion = function (questionNumber, preventAnnouncement) { // Sanitize input. if (questionNumber < 0) { questionNumber = 0; } if (questionNumber >= params.questions.length) { questionNumber = params.questions.length - 1; } currentQuestion = questionNumber; // Hide all questions $('.question-container', $myDom).hide().eq(questionNumber).show(); if (questionInstances[questionNumber]) { // Trigger resize on question in case the size of the QS has changed. var instance = questionInstances[questionNumber]; instance.setActivityStarted(); if (instance.$ !== undefined) { instance.trigger('resize'); } } // Update progress indicator // Test if current has been answered. if (params.progressType === 'textual') { $('.progress-text', $myDom).text(params.texts.textualProgress.replace("@current", questionNumber+1).replace("@total", params.questions.length)); } else { // Set currentNess var previousQuestion = $('.progress-dot.current', $myDom).parent().index(); if (previousQuestion >= 0) { toggleCurrentDot(previousQuestion, false); toggleAnsweredDot(previousQuestion, questionInstances[previousQuestion].getAnswerGiven()); } toggleCurrentDot(questionNumber, true); } if (!preventAnnouncement) { // Announce question number of total, must use timeout because of buttons logic setTimeout(function () { var humanizedProgress = params.texts.readSpeakerProgress .replace('@current', (currentQuestion + 1).toString()) .replace('@total', questionInstances.length.toString()); self.$progressAnnouncer.html(humanizedProgress); if (instance && instance.readFeedback) { instance.readFeedback(); } }, 0); } // Remember where we are _updateButtons(); self.trigger('resize'); return currentQuestion; }; /** * Show solutions for subcontent, and hide subcontent buttons. * Used for contracts with integrated content. * @public */ var showSolutions = function () { showingSolutions = true; for (var i = 0; i < questionInstances.length; i++) { // Enable back and forth navigation in solution mode toggleDotsNavigation(true); if (i < questionInstances.length - 1) { questionInstances[i].showButton('next'); } if (i > 0) { questionInstances[i].showButton('prev'); } try { // Do not read answers questionInstances[i].toggleReadSpeaker(true); questionInstances[i].showSolutions(); questionInstances[i].toggleReadSpeaker(false); } catch (error) { H5P.error("subcontent does not contain a valid showSolutions function"); H5P.error(error); } } }; /** * Toggles whether dots are enabled for navigation */ var toggleDotsNavigation = function (enable) { $('.progress-dot', $myDom).each(function () { $(this).toggleClass('disabled', !enable); $(this).attr('aria-disabled', enable ? 'false' : 'true'); // Remove tabindex if (!enable) { $(this).attr('tabindex', '-1'); } }); }; /** * Resets the task and every subcontent task. * Used for contracts with integrated content. * @public * @param {boolean} moveFocus True to move the focus to first option * This prevents loss of focus if reset from within content */ this.resetTask = function (moveFocus = false) { // Clear previous state to ensure questions are created cleanly contentData.previousState = {}; self.hasPrevState = false; questionOrder = undefined; showingSolutions = false; for (var i = 0; i < questionInstances.length; i++) { try { questionInstances[i].resetTask(); // Hide back and forth navigation in normal mode if (params.disableBackwardsNavigation) { toggleDotsNavigation(false); // Check if first question is answered by default if (i === 0 && questionInstances[i].getAnswerGiven()) { questionInstances[i].showButton('next'); } else { questionInstances[i].hideButton('next'); } questionInstances[i].hideButton('prev'); } } catch (error) { H5P.error("subcontent does not contain a valid resetTask function"); H5P.error(error); } } // Hide finish button questionInstances[questionInstances.length - 1].hideButton('finish'); // Mark all tasks as unanswered: $('.progress-dot').each(function (idx) { toggleAnsweredDot(idx, false); }); //Force the last page to be reRendered rendered = false; if (params.poolSize > 0) { // Make new pool from params.questions // Randomize and get the results var poolResult = randomizeQuestionOrdering(initialParams.questions); var poolQuestions = poolResult.questions; poolOrder = poolResult.questionOrder; // Discard extra questions poolQuestions = poolQuestions.slice(0, params.poolSize); poolOrder = poolOrder.slice(0, params.poolSize); // Replace original questions with just the ones in the pool params.questions = poolQuestions; // Recreate the question instances questionInstances = createQuestionInstancesFromQuestions(params.questions); // Update buttons initializeQuestion(); } else if (params.randomQuestions) { randomizeQuestions(); } // Reset currentQuestion currentQuestion = 0; $myDom.children().hide(); var $intro = $('.intro-page', $myDom); if ($intro.length) { // Show intro $('.intro-page', $myDom).show(); if (moveFocus) { $('.qs-startbutton', $myDom).focus(); } } else { // Show first question $('.questionset', $myDom).show(); _showQuestion(params.initialQuestion); if (moveFocus) { // Focus first tabbable element $myDom[0].querySelectorAll( 'audio, button, input, select, textarea, video, [contenteditable], [href], [tabindex="0"]' )[0].focus(); } } }; var rendered = false; this.reRender = function () { rendered = false; }; /** * Randomizes question instances */ var randomizeQuestions = function () { // Recreate questioninstances in original order questionInstances = createQuestionInstancesFromQuestions(params.questions); // Scramble them var result = randomizeQuestionOrdering(questionInstances); questionInstances = result.questions; questionOrder = result.questionOrder; // Update buttons initializeQuestion(); }; var moveQuestion = function (direction) { if (params.disableBackwardsNavigation && !questionInstances[currentQuestion].getAnswerGiven()) { questionInstances[currentQuestion].hideButton('next'); questionInstances[currentQuestion].hideButton('finish'); return; } if (currentQuestion + direction >= questionInstances.length) { toggleCurrentDot(currentQuestion, false); toggleAnsweredDot(currentQuestion, questionInstances[currentQuestion].getAnswerGiven()); _displayEndGame(); } else { // Allow movement if backward navigation enabled or answer given _showQuestion(currentQuestion + direction); } // Trigger xAPI self.triggerXAPIProgressed(); }; /** * Toggle answered state of dot at given index * @param {number} dotIndex Index of dot * @param {boolean} isAnswered True if is answered, False if not answered */ var toggleAnsweredDot = function (dotIndex, isAnswered) { var $el = $('.progress-dot:eq(' + dotIndex +')', $myDom); // Skip current button if ($el.hasClass('current')) { return; } // Ensure boolean isAnswered = !!isAnswered; var label = params.texts.jumpToQuestion .replace('%d', (dotIndex + 1).toString()) .replace('%total', $('.progress-dot', $myDom).length) + ', ' + (isAnswered ? params.texts.answeredText : params.texts.unansweredText); $el.toggleClass('unanswered', !isAnswered) .toggleClass('answered', isAnswered) .attr('aria-label', label); }; /** * Toggle current state of dot at given index * @param dotIndex * @param isCurrent */ var toggleCurrentDot = function (dotIndex, isCurrent) { var $el = $('.progress-dot:eq(' + dotIndex +')', $myDom); var texts = params.texts; var label = texts.jumpToQuestion .replace('%d', (dotIndex + 1).toString()) .replace('%total', $('.progress-dot', $myDom).length); if (!isCurrent) { var isAnswered = $el.hasClass('answered'); label += ', ' + (isAnswered ? texts.answeredText : texts.unansweredText); } else { label += ', ' + texts.currentQuestionText; } var disabledTabindex = params.disableBackwardsNavigation && !showingSolutions; $el.toggleClass('current', isCurrent) .attr('aria-label', label) .attr('tabindex', isCurrent && !disabledTabindex ? 0 : -1) .attr('aria-selected', isCurrent && !disabledTabindex); }; var _displayEndGame = function () { $('.progress-dot.current', $myDom).removeClass('current'); if (rendered) { $myDom.children().hide().filter('.questionset-results').show(); self.trigger('resize'); return; } //Remove old score screen. $myDom.children().hide().filter('.questionset-results').remove(); rendered = true; // Get total score. var finals = self.getScore(); var totals = self.getMaxScore(); var scoreString = H5P.Question.determineOverallFeedback(params.endGame.overallFeedback, finals / totals).replace('@score', finals).replace('@total', totals); var success = ((100 * finals / totals) >= params.passPercentage); /** * Makes our buttons behave like other buttons. * * @private * @param {string} classSelector * @param {function} handler */ var hookUpButton = function (classSelector, handler) { $(classSelector, $myDom).click(handler).keypress(function (e) { if (e.which === 32) { handler(); e.preventDefault(); } }); }; var displayResults = function () { self.triggerXAPICompleted(self.getScore(), self.getMaxScore(), success); var eparams = { message: params.endGame.showResultPage ? params.endGame.message : params.endGame.noResultMessage, comment: params.endGame.showResultPage ? (success ? params.endGame.oldFeedback.successGreeting : params.endGame.oldFeedback.failGreeting) : undefined, resulttext: params.endGame.showResultPage ? (success ? params.endGame.oldFeedback.successComment : params.endGame.oldFeedback.failComment) : undefined, finishButtonText: (self.isSubmitting) ? params.endGame.submitButtonText : params.endGame.finishButtonText, solutionButtonText: params.endGame.solutionButtonText, retryButtonText: params.endGame.retryButtonText }; // Create html for end screen self.$resultPage = $('<div>', { 'class': 'questionset-results' }); $('<div>', { class: 'greeting', html: eparams.message, appendTo: self.$resultPage }); $('<div>', { class: 'feedback-section', html: '<div class="feedback-scorebar"></div>' + '<div class="feedback-text"></div>', appendTo: self.$resultPage }); if (params.comment) { $('<div>', { 'class': 'result-header', html: eparams.comment, appendTo: self.$resultPage }); } if (params.resulttext) { $('<div>', { class: 'result-text', html: eparams.resulttext, appendTo: self.$resultPage }); } self.$buttonsContainer = $('<div>', { class: 'buttons', appendTo: self.$resultPage }); if (params.endGame.showSolutionButton) { $('<button>', { class: 'h5p-joubelui-button h5p-button qs-solutionbutton', type: 'button', html: eparams.solutionButtonText, appendTo: self.$buttonsContainer }); } if (params.endGame.showRetryButton) { $('<button>', { class: 'h5p-joubelui-button h5p-button qs-retrybutton', type: 'button', html: eparams.retryButtonText, appendTo: self.$buttonsContainer }) } // Show result page. $myDom.children().hide(); $myDom.append(self.$resultPage); if (params.endGame.showResultPage) { hookUpButton('.qs-solutionbutton', function () { showSolutions(); $myDom.children().hide().filter('.questionset').show(); _showQuestion(params.initialQuestion); }); hookUpButton('.qs-retrybutton', function () { self.resetTask(true); }); if (scoreBar === undefined) { scoreBar = H5P.JoubelUI.createScoreBar(totals); } scoreBar.appendTo($('.feedback-scorebar', $myDom)); $('.feedback-text', $myDom).html(scoreString); // Announce that the question set is complete setTimeout(function () { self.$progressAnnouncer .html(eparams.message + scoreString + '.' + (params.endGame.scoreBarLabel).replace('@finals', finals).replace('@totals', totals) + '.' + eparams.comment + '.' + eparams.resulttext); scoreBar.setMaxScore(totals); scoreBar.setScore(finals); }, 0); } else { // Remove buttons and feedback section $('.qs-solutionbutton, .qs-retrybutton, .feedback-section', $myDom).remove(); } self.trigger('resize'); }; if (params.endGame.showAnimations) { var videoData = success ? params.endGame.successVideo : params.endGame.failVideo; if (videoData) { $myDom.children().hide(); var $videoContainer = $('<div class="video-container"></div>').appendTo($myDom); var video = new H5P.Video({ sources: videoData, fitToWrapper: true, controls: false, autoplay: false }, contentId); video.on('stateChange', function (event) { if (event.data === H5P.Video.ENDED) { displayResults(); $videoContainer.hide(); } }); video.attach($videoContainer); // Resize on video loaded video.on('loaded', function () { self.trigger('resize'); }); video.play(); if (params.endGame.skippable) { $('<a class="h5p-joubelui-button h5p-button skip">' + params.endGame.skipButtonText + '</a>').click(function () { video.pause(); $videoContainer.hide(); displayResults(); }).appendTo($videoContainer); } return; } } // Trigger finished event. displayResults(); self.trigger('resize'); }; var registerImageLoadedListener = function (question) { H5P.on(question, 'imageLoaded', function () { self.trigger('resize'); }); }; /** * Initialize a question and attach it to the DOM * */ function initializeQuestion() { // Attach questions for (var i = 0; i < questionInstances.length; i++) { var question = questionInstances[i]; // Make sure styles are not being added twice $('.question-container:eq(' + i + ')', $myDom).attr('class', 'question-container'); question.attach($('.question-container:eq(' + i + ')', $myDom)); // Listen for image resize registerImageLoadedListener(question); // Add finish button const finishButtonText = (self.isSubmitting) ? params.texts.submitButton : params.texts.finishButton question.addButton('finish', finishButtonText, moveQuestion.bind(this, 1), false); // Add next button question.addButton('next', '', moveQuestion.bind(this, 1), !params.disableBackwardsNavigation || !!question.getAnswerGiven(), { href: '#', // Use href since this is a navigation button 'aria-label': params.texts.nextButton }); // Add previous button question.addButton('prev', '', moveQuestion.bind(this, -1), !(questionInstances[0] === question || params.disableBackwardsNavigation), { href: '#', // Use href since this is a navigation buttonq 'aria-label': params.texts.prevButton }); // Hide next button if it is the last question if (questionInstances[questionInstances.length -1] === question) { question.hideButton('next'); } question.on('xAPI', function (event) { var shortVerb = event.getVerb(); if (shortVerb === 'interacted' || shortVerb === 'answered' || shortVerb === 'attempted') { toggleAnsweredDot(currentQuestion, questionInstances[currentQuestion].getAnswerGiven()); _updateButtons(); } if (shortVerb === 'completed') { // An activity within this activity is not allowed to send completed events event.setVerb('answered'); } if (event.data.statement.context.extensions === undefined) { event.data.statement.context.extensions = {}; } event.data.statement.context.extensions['http://id.tincanapi.com/extension/ending-point'] = currentQuestion + 1; }); // Mark question if answered toggleAnsweredDot(i, question.getAnswerGiven()); } } this.attach = function (target) { if (this.isRoot()) { this.setActivityStarted(); } if (typeof(target) === "string") { $myDom = $('#' + target); } else { $myDom = $(target); } // Render own DOM into target. $myDom.children().remove(); $myDom.append(self.$introPage, self.$questionsContainer); $myDom.parent().append(self.$progressAnnouncer); if (params.backgroundImage !== undefined) { $myDom.css({ overflow: 'hidden', background: '#fff url("' + H5P.getPath(params.backgroundImage.path, contentId) + '") no-repeat 50% 50%', backgroundSize: '100% auto' }); } if (params.introPage.backgroundImage !== undefined) { var $intro = $myDom.find('.intro-page'); if ($intro.length) { var bgImg = params.introPage.backgroundImage; var bgImgRatio = (bgImg.height / bgImg.width); $intro.css({ background: '#fff url("' + H5P.getPath(bgImg.path, contentId) + '") no-repeat 50% 50%', backgroundSize: 'auto 100%', minHeight: bgImgRatio * +window.getComputedStyle($intro[0]).width.replace('px','') }); } } initializeQuestion(); // Allow other libraries to add transitions after the questions have been inited $('.questionset', $myDom).addClass('started'); $('.qs-startbutton', $myDom) .click(function () { $(this).parents('.intro-page').hide(); $('.questionset', $myDom).show(); _showQuestion(params.initialQuestion); event.preventDefault(); }) .keydown(function (event) { switch (event.which) { case 13: // Enter case 32: // Space $(this).parents('.intro-page').hide(); $('.questionset', $myDom).show(); _showQuestion(params.initialQuestion); event.preventDefault(); } }); /** * Triggers changing the current question. * * @private * @param {Object} [event] */ var handleProgressDotClick = function (event) { // Disable dots when backward nav disabled event.preventDefault(); if (params.disableBackwardsNavigation && !showingSolutions) { return; } _showQuestion($(this).parent().index()); // Trigger xAPI self.triggerXAPIProgressed(); }; // Set event listeners. $('.progress-dot', $myDom).click(handleProgressDotClick).keydown(function (event) { var $this = $(this); switch (event.which) { case 13: // Enter case 32: // Space handleProgressDotClick.call(this, event); break; case 37: // Left Arrow case 38: // Up Arrow // Go to previous dot var $prev = $this.parent().prev(); if ($prev.length) { $prev.children('a').attr('tabindex', '0').focus(); $this.attr('tabindex', '-1'); } break; case 39: // Right Arrow case 40: // Down Arrow // Go to next dot var $next = $this.parent().next(); if ($next.length) { $next.children('a').attr('tabindex', '0').focus(); $this.attr('tabindex', '-1'); } break; } }); // Hide all but current question _showQuestion(currentQuestion, true); if (renderSolutions) { showSolutions(); } // Update buttons in case they have changed (restored user state) _updateButtons(); this.trigger('resize'); return this; }; // Get current score for questionset. this.getScore = function () { var score = 0; for (var i = questionInstances.length - 1; i >= 0; i--) { score += questionInstances[i].getScore(); } return score; }; // Get total score possible for questionset. this.getMaxScore = function () { var score = 0; for (var i = questionInstances.length - 1; i >= 0; i--) { score += questionInstances[i].getMaxScore(); } return score; }; /** * @deprecated since version 1.9.2 * @returns {number} */ this.totalScore = function () { return this.getMaxScore(); }; /** * Gather copyright information for the current content. * * @returns {H5P.ContentCopyrights} */ this.getCopyrights = function () { var info = new H5P.ContentCopyrights(); // IntroPage Background if (params.introPage !== undefined && params.introPage.backgroundImage !== undefined && params.introPage.backgroundImage.copyright !== undefined) { var introBackground = new H5P.MediaCopyright(params.introPage.backgroundImage.copyright); introBackground.setThumbnail(new H5P.Thumbnail(H5P.getPath(params.introPage.backgroundImage.path, contentId), params.introPage.backgroundImage.width, params.introPage.backgroundImage.height)); info.addMedia(introBackground); } // Background if (params.backgroundImage !== undefined && params.backgroundImage.copyright !== undefined) { var background = new H5P.MediaCopyright(params.backgroundImage.copyright); background.setThumbnail(new H5P.Thumbnail(H5P.getPath(params.backgroundImage.path, contentId), params.backgroundImage.width, params.backgroundImage.height)); info.addMedia(background); } // Questions var questionCopyrights; for (var i = 0; i < questionInstances.length; i++) { var instance = questionInstances[i]; var instanceParams = params.questions[i].params; questionCopyrights = undefined; if (instance.getCopyrights !== undefined) { // Use the instance's own copyright generator questionCopyrights = instance.getCopyrights(); } if (questionCopyrights === undefined) { // Create a generic flat copyright list questionCopyrights = new H5P.ContentCopyrights(); H5P.findCopyrights(questionCopyrights, instanceParams.params, contentId,{ metadata: instanceParams.metadata, machineName: instanceParams.library.split(' ')[0] }); } // Determine label var label = (params.texts.questionLabel + ' ' + (i + 1)); if (instanceParams.params.contentName !== undefined) { label += ': ' + instanceParams.params.contentName; } else if (instance.getTitle !== undefined) { label += ': ' + instance.getTitle(); } questionCopyrights.setLabel(label); // Add info info.addContent(questionCopyrights); } // Success video var video; if (params.endGame.successVideo !== undefined && params.endGame.successVideo.length > 0) { video = params.endGame.successVideo[0]; if (video.copyright !== undefined) { info.addMedia(new H5P.MediaCopyright(video.copyright)); } } // Fail video if (params.endGame.failVideo !== undefined && params.endGame.failVideo.length > 0) { video = params.endGame.failVideo[0]; if (video.copyright !== undefined) { info.addMedia(new H5P.MediaCopyright(video.copyright)); } } return info; }; this.getQuestions = function () { return questionInstances; }; this.showSolutions = function () { renderSolutions = true; }; /** * Returns the complete state of question set and sub-content * * @returns {Object} current state */ this.getCurrentState = function () { const progress = showingSolutions ? questionInstances.length - 1 : currentQuestion; const answers = questionInstances.map(function (qi) { return qi.getCurrentState(); }); // If the user has moved past the first question, if the content has been resumed, // or if at least one of the answers to the questions are considered not empty. if (progress || self.hasPrevState || answers.some(answer => !H5P.isEmpty(answer))) { return { progress: progress, answers: answers, order: questionOrder, poolOrder: poolOrder, }; } return {}; }; /** * Trigger the xAPI progressed event */ this.triggerXAPIProgressed = function () { const progressedEvent = this.createXAPIEventTemplate('progressed'); if (progressedEvent.data.statement.context.extensions === undefined) { progressedEvent.data.statement.context.extensions = {}; } progressedEvent.data.statement.context.extensions['http://id.tincanapi.com/extension/ending-point'] = currentQuestion + 1; this.trigger(progressedEvent); } /** * Generate xAPI object definition used in xAPI statements. * @return {Object} */ var getxAPIDefinition = function () { var definition = {}; definition.interactionType = 'compound'; definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction'; definition.description = { 'en-US': '' }; return definition; }; /** * Add the question itself to the definition part of an xAPIEvent */ var addQuestionToXAPI = function (xAPIEvent) { var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']); $.extend(definition, getxAPIDefinition()); }; /** * Get xAPI data from sub content types * * @param {Object} metaContentType * @returns {array} */ var getXAPIDataFromChildren = function (metaContentType) { return metaContentType.getQuestions().map(function (question) { return question.getXAPIData(); }); }; /** * Get xAPI data. * Contract used by report rendering engine. * * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6} */ this.getXAPIData = function () { var xAPIEvent = this.createXAPIEventTemplate('answered'); addQuestionToXAPI(xAPIEvent); xAPIEvent.setScoredResult(this.getScore(), this.getMaxScore(), this, true, this.getScore() === this.getMaxScore() ); return { statement: xAPIEvent.data.statement, children: getXAPIDataFromChildren(this) }; }; /** * Get context data. * Contract used for confusion report. */ this.getContext = function () { // Get question index and add 1, count starts from 0 let contextObject = { type: 'question', value: (currentQuestion + 1) }; // Send actual index of the question if questions are randomized if (params.randomQuestions) { contextObject.actual = questionOrder[currentQuestion] + 1; } return contextObject; }; }; H5P.QuestionSet.prototype = Object.create(H5P.EventDispatcher.prototype); H5P.QuestionSet.prototype.constructor = H5P.QuestionSet; ; /*global H5P*/ /** * Graph Cake module * @external {jQuery} $ H5P.jQuery * @external {EventDispatcher} EventDispatcher H5P.EventDispatcher */ H5P.Chart = (function ($, EventDispatcher) { /** * Initialize module. * * @class * @param {Object} params Behavior settings * @param {Number} id Content identification */ function Chart(params) { var self = this; // Inheritance EventDispatcher.call(self); // Set params and filter data set to make sure it's valid self.params = params; if (self.params.listOfTypes) { Chart.filterData(self.params.listOfTypes); } else { self.params.listOfTypes = []; } // Add example/default behavior if (!self.params.listOfTypes.length) { self.params.listOfTypes = [ { text: 'Cat', value: 1, color: '#D3D3D3', fontColor: '#000' }, { text: 'Dog', value: 2, color: '#ADD8E6', fontColor: '#000' }, { text: 'Mouse', value: 3, color: '#90EE90', fontColor: '#000' } ]; } // Set the figure definition for readspeakers if it doesn't exist if (!self.params.figureDefinition) { self.params.figureDefinition = "Chart"; } // Keep track of type. self.type = (self.params.graphMode === 'pieChart' ? 'Pie' : 'Bar'); } // Inheritance Chart.prototype = Object.create(EventDispatcher.prototype); Chart.prototype.constructor = Chart; /** * Make sure the data set has set the required text and value properties. * * @param {Array} dataSet */ Chart.filterData = function (dataSet) { // Cycle through data set for (var i = 0; i < dataSet.length; i++) { var row = dataSet[i]; if (row.text === undefined || row.value === undefined) { // Remove invalid data dataSet.splice(i, 1); i--; continue; } row.text = this.htmlDecode(row.text.trim()); row.value = parseFloat(row.value); if (row.text === '' || isNaN(row.value)) { // Remove invalid data dataSet.splice(i, 1); i--; continue; } } }; /** * Retrieve true string from HTML encoded string. * @param {string} input Input string. * @return {string} Output string. */ Chart.htmlDecode = function (input) { const dparser = new DOMParser().parseFromString(input, 'text/html'); const div = document.createElement('div'); div.innerHTML = dparser.documentElement.textContent; return div.textContent || div.innerText || ''; } /** * Append field to wrapper. * * @param {H5P.jQuery} $container */ Chart.prototype.attach = function ($container) { var self = this; // Create chart on first attach if (self.$wrapper === undefined) { self.$wrapper = $('<div/>', { 'class': 'h5p-chart-chart h5p-chart-' + self.type.toLowerCase() }); self.chart = new H5P.Chart[self.type + 'Chart'](self.params, self.$wrapper); } // Prepare container self.$container = $container.html('').addClass('h5p-chart').append(self.$wrapper); const $defgroup = $('<div/>', { 'class': 'hidden-but-read', 'html': self.params.figureDefinition, }); // Add aria-labels for the data self.params.listOfTypes.forEach(function(type) { var ariaLabel = $('<div/>', { 'class': 'hidden-but-read', 'html': type.text + ': ' + type.value + '' }); $defgroup.append(ariaLabel); }); self.$container.append($defgroup); // Handle resizing self.on('resize', function () { if (!self.$container.is(':visible')) { return; // Only handle if visible } // Resize existing chart self.chart.resize(); }); }; return Chart; })(H5P.jQuery, H5P.EventDispatcher); ; !function() { var d3 = { version: "3.5.2" }; if (!Date.now) Date.now = function() { return +new Date(); }; var d3_arraySlice = [].slice, d3_array = function(list) { return d3_arraySlice.call(list); }; var d3_document = document, d3_documentElement = d3_document.documentElement, d3_window = window; try { d3_array(d3_documentElement.childNodes)[0].nodeType; } catch (e) { d3_array = function(list) { var i = list.length, array = new Array(i); while (i--) array[i] = list[i]; return array; }; } try { d3_document.createElement("div").style.setProperty("opacity", 0, ""); } catch (error) { var d3_element_prototype = d3_window.Element.prototype, d3_element_setAttribute = d3_element_prototype.setAttribute, d3_element_setAttributeNS = d3_element_prototype.setAttributeNS, d3_style_prototype = d3_window.CSSStyleDeclaration.prototype, d3_style_setProperty = d3_style_prototype.setProperty; d3_element_prototype.setAttribute = function(name, value) { d3_element_setAttribute.call(this, name, value + ""); }; d3_element_prototype.setAttributeNS = function(space, local, value) { d3_element_setAttributeNS.call(this, space, local, value + ""); }; d3_style_prototype.setProperty = function(name, value, priority) { d3_style_setProperty.call(this, name, value + "", priority); }; } d3.ascending = d3_ascending; function d3_ascending(a, b) { return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN; } d3.descending = function(a, b) { return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN; }; d3.min = function(array, f) { var i = -1, n = array.length, a, b; if (arguments.length === 1) { while (++i < n) if ((b = array[i]) != null && b >= b) { a = b; break; } while (++i < n) if ((b = array[i]) != null && a > b) a = b; } else { while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { a = b; break; } while (++i < n) if ((b = f.call(array, array[i], i)) != null && a > b) a = b; } return a; }; d3.max = function(array, f) { var i = -1, n = array.length, a, b; if (arguments.length === 1) { while (++i < n) if ((b = array[i]) != null && b >= b) { a = b; break; } while (++i < n) if ((b = array[i]) != null && b > a) a = b; } else { while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { a = b; break; } while (++i < n) if ((b = f.call(array, array[i], i)) != null && b > a) a = b; } return a; }; d3.extent = function(array, f) { var i = -1, n = array.length, a, b, c; if (arguments.length === 1) { while (++i < n) if ((b = array[i]) != null && b >= b) { a = c = b; break; } while (++i < n) if ((b = array[i]) != null) { if (a > b) a = b; if (c < b) c = b; } } else { while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { a = c = b; break; } while (++i < n) if ((b = f.call(array, array[i], i)) != null) { if (a > b) a = b; if (c < b) c = b; } } return [ a, c ]; }; function d3_number(x) { return x === null ? NaN : +x; } function d3_numeric(x) { return !isNaN(x); } d3.sum = function(array, f) { var s = 0, n = array.length, a, i = -1; if (arguments.length === 1) { while (++i < n) if (d3_numeric(a = +array[i])) s += a; } else { while (++i < n) if (d3_numeric(a = +f.call(array, array[i], i))) s += a; } return s; }; d3.mean = function(array, f) { var s = 0, n = array.length, a, i = -1, j = n; if (arguments.length === 1) { while (++i < n) if (d3_numeric(a = d3_number(array[i]))) s += a; else --j; } else { while (++i < n) if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) s += a; else --j; } if (j) return s / j; }; d3.quantile = function(values, p) { var H = (values.length - 1) * p + 1, h = Math.floor(H), v = +values[h - 1], e = H - h; return e ? v + e * (values[h] - v) : v; }; d3.median = function(array, f) { var numbers = [], n = array.length, a, i = -1; if (arguments.length === 1) { while (++i < n) if (d3_numeric(a = d3_number(array[i]))) numbers.push(a); } else { while (++i < n) if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) numbers.push(a); } if (numbers.length) return d3.quantile(numbers.sort(d3_ascending), .5); }; d3.variance = function(array, f) { var n = array.length, m = 0, a, d, s = 0, i = -1, j = 0; if (arguments.length === 1) { while (++i < n) { if (d3_numeric(a = d3_number(array[i]))) { d = a - m; m += d / ++j; s += d * (a - m); } } } else { while (++i < n) { if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) { d = a - m; m += d / ++j; s += d * (a - m); } } } if (j > 1) return s / (j - 1); }; d3.deviation = function() { var v = d3.variance.apply(this, arguments); return v ? Math.sqrt(v) : v; }; function d3_bisector(compare) { return { left: function(a, x, lo, hi) { if (arguments.length < 3) lo = 0; if (arguments.length < 4) hi = a.length; while (lo < hi) { var mid = lo + hi >>> 1; if (compare(a[mid], x) < 0) lo = mid + 1; else hi = mid; } return lo; }, right: function(a, x, lo, hi) { if (arguments.length < 3) lo = 0; if (arguments.length < 4) hi = a.length; while (lo < hi) { var mid = lo + hi >>> 1; if (compare(a[mid], x) > 0) hi = mid; else lo = mid + 1; } return lo; } }; } var d3_bisect = d3_bisector(d3_ascending); d3.bisectLeft = d3_bisect.left; d3.bisect = d3.bisectRight = d3_bisect.right; d3.bisector = function(f) { return d3_bisector(f.length === 1 ? function(d, x) { return d3_ascending(f(d), x); } : f); }; d3.shuffle = function(array, i0, i1) { if ((m = arguments.length) < 3) { i1 = array.length; if (m < 2) i0 = 0; } var m = i1 - i0, t, i; while (m) { i = Math.random() * m-- | 0; t = array[m + i0], array[m + i0] = array[i + i0], array[i + i0] = t; } return array; }; d3.permute = function(array, indexes) { var i = indexes.length, permutes = new Array(i); while (i--) permutes[i] = array[indexes[i]]; return permutes; }; d3.pairs = function(array) { var i = 0, n = array.length - 1, p0, p1 = array[0], pairs = new Array(n < 0 ? 0 : n); while (i < n) pairs[i] = [ p0 = p1, p1 = array[++i] ]; return pairs; }; d3.zip = function() { if (!(n = arguments.length)) return []; for (var i = -1, m = d3.min(arguments, d3_zipLength), zips = new Array(m); ++i < m; ) { for (var j = -1, n, zip = zips[i] = new Array(n); ++j < n; ) { zip[j] = arguments[j][i]; } } return zips; }; function d3_zipLength(d) { return d.length; } d3.transpose = function(matrix) { return d3.zip.apply(d3, matrix); }; d3.keys = function(map) { var keys = []; for (var key in map) keys.push(key); return keys; }; d3.values = function(map) { var values = []; for (var key in map) values.push(map[key]); return values; }; d3.entries = function(map) { var entries = []; for (var key in map) entries.push({ key: key, value: map[key] }); return entries; }; d3.merge = function(arrays) { var n = arrays.length, m, i = -1, j = 0, merged, array; while (++i < n) j += arrays[i].length; merged = new Array(j); while (--n >= 0) { array = arrays[n]; m = array.length; while (--m >= 0) { merged[--j] = array[m]; } } return merged; }; var abs = Math.abs; d3.range = function(start, stop, step) { if (arguments.length < 3) { step = 1; if (arguments.length < 2) { stop = start; start = 0; } } if ((stop - start) / step === Infinity) throw new Error("infinite range"); var range = [], k = d3_range_integerScale(abs(step)), i = -1, j; start *= k, stop *= k, step *= k; if (step < 0) while ((j = start + step * ++i) > stop) range.push(j / k); else while ((j = start + step * ++i) < stop) range.push(j / k); return range; }; function d3_range_integerScale(x) { var k = 1; while (x * k % 1) k *= 10; return k; } function d3_class(ctor, properties) { for (var key in properties) { Object.defineProperty(ctor.prototype, key, { value: properties[key], enumerable: false }); } } d3.map = function(object, f) { var map = new d3_Map(); if (object instanceof d3_Map) { object.forEach(function(key, value) { map.set(key, value); }); } else if (Array.isArray(object)) { var i = -1, n = object.length, o; if (arguments.length === 1) while (++i < n) map.set(i, object[i]); else while (++i < n) map.set(f.call(object, o = object[i], i), o); } else { for (var key in object) map.set(key, object[key]); } return map; }; function d3_Map() { this._ = Object.create(null); } var d3_map_proto = "__proto__", d3_map_zero = "\x00"; d3_class(d3_Map, { has: d3_map_has, get: function(key) { return this._[d3_map_escape(key)]; }, set: function(key, value) { return this._[d3_map_escape(key)] = value; }, remove: d3_map_remove, keys: d3_map_keys, values: function() { var values = []; for (var key in this._) values.push(this._[key]); return values; }, entries: function() { var entries = []; for (var key in this._) entries.push({ key: d3_map_unescape(key), value: this._[key] }); return entries; }, size: d3_map_size, empty: d3_map_empty, forEach: function(f) { for (var key in this._) f.call(this, d3_map_unescape(key), this._[key]); } }); function d3_map_escape(key) { return (key += "") === d3_map_proto || key[0] === d3_map_zero ? d3_map_zero + key : key; } function d3_map_unescape(key) { return (key += "")[0] === d3_map_zero ? key.slice(1) : key; } function d3_map_has(key) { return d3_map_escape(key) in this._; } function d3_map_remove(key) { return (key = d3_map_escape(key)) in this._ && delete this._[key]; } function d3_map_keys() { var keys = []; for (var key in this._) keys.push(d3_map_unescape(key)); return keys; } function d3_map_size() { var size = 0; for (var key in this._) ++size; return size; } function d3_map_empty() { for (var key in this._) return false; return true; } d3.nest = function() { var nest = {}, keys = [], sortKeys = [], sortValues, rollup; function map(mapType, array, depth) { if (depth >= keys.length) return rollup ? rollup.call(nest, array) : sortValues ? array.sort(sortValues) : array; var i = -1, n = array.length, key = keys[depth++], keyValue, object, setter, valuesByKey = new d3_Map(), values; while (++i < n) { if (values = valuesByKey.get(keyValue = key(object = array[i]))) { values.push(object); } else { valuesByKey.set(keyValue, [ object ]); } } if (mapType) { object = mapType(); setter = function(keyValue, values) { object.set(keyValue, map(mapType, values, depth)); }; } else { object = {}; setter = function(keyValue, values) { object[keyValue] = map(mapType, values, depth); }; } valuesByKey.forEach(setter); return object; } function entries(map, depth) { if (depth >= keys.length) return map; var array = [], sortKey = sortKeys[depth++]; map.forEach(function(key, keyMap) { array.push({ key: key, values: entries(keyMap, depth) }); }); return sortKey ? array.sort(function(a, b) { return sortKey(a.key, b.key); }) : array; } nest.map = function(array, mapType) { return map(mapType, array, 0); }; nest.entries = function(array) { return entries(map(d3.map, array, 0), 0); }; nest.key = function(d) { keys.push(d); return nest; }; nest.sortKeys = function(order) { sortKeys[keys.length - 1] = order; return nest; }; nest.sortValues = function(order) { sortValues = order; return nest; }; nest.rollup = function(f) { rollup = f; return nest; }; return nest; }; d3.set = function(array) { var set = new d3_Set(); if (array) for (var i = 0, n = array.length; i < n; ++i) set.add(array[i]); return set; }; function d3_Set() { this._ = Object.create(null); } d3_class(d3_Set, { has: d3_map_has, add: function(key) { this._[d3_map_escape(key += "")] = true; return key; }, remove: d3_map_remove, values: d3_map_keys, size: d3_map_size, empty: d3_map_empty, forEach: function(f) { for (var key in this._) f.call(this, d3_map_unescape(key)); } }); d3.behavior = {}; d3.rebind = function(target, source) { var i = 1, n = arguments.length, method; while (++i < n) target[method = arguments[i]] = d3_rebind(target, source, source[method]); return target; }; function d3_rebind(target, source, method) { return function() { var value = method.apply(source, arguments); return value === source ? target : value; }; } function d3_vendorSymbol(object, name) { if (name in object) return name; name = name.charAt(0).toUpperCase() + name.slice(1); for (var i = 0, n = d3_vendorPrefixes.length; i < n; ++i) { var prefixName = d3_vendorPrefixes[i] + name; if (prefixName in object) return prefixName; } } var d3_vendorPrefixes = [ "webkit", "ms", "moz", "Moz", "o", "O" ]; function d3_noop() {} d3.dispatch = function() { var dispatch = new d3_dispatch(), i = -1, n = arguments.length; while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch); return dispatch; }; function d3_dispatch() {} d3_dispatch.prototype.on = function(type, listener) { var i = type.indexOf("."), name = ""; if (i >= 0) { name = type.slice(i + 1); type = type.slice(0, i); } if (type) return arguments.length < 2 ? this[type].on(name) : this[type].on(name, listener); if (arguments.length === 2) { if (listener == null) for (type in this) { if (this.hasOwnProperty(type)) this[type].on(name, null); } return this; } }; function d3_dispatch_event(dispatch) { var listeners = [], listenerByName = new d3_Map(); function event() { var z = listeners, i = -1, n = z.length, l; while (++i < n) if (l = z[i].on) l.apply(this, arguments); return dispatch; } event.on = function(name, listener) { var l = listenerByName.get(name), i; if (arguments.length < 2) return l && l.on; if (l) { l.on = null; listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1)); listenerByName.remove(name); } if (listener) listeners.push(listenerByName.set(name, { on: listener })); return dispatch; }; return event; } d3.event = null; function d3_eventPreventDefault() { d3.event.preventDefault(); } function d3_eventSource() { var e = d3.event, s; while (s = e.sourceEvent) e = s; return e; } function d3_eventDispatch(target) { var dispatch = new d3_dispatch(), i = 0, n = arguments.length; while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch); dispatch.of = function(thiz, argumentz) { return function(e1) { try { var e0 = e1.sourceEvent = d3.event; e1.target = target; d3.event = e1; dispatch[e1.type].apply(thiz, argumentz); } finally { d3.event = e0; } }; }; return dispatch; } d3.requote = function(s) { return s.replace(d3_requote_re, "\\$&"); }; var d3_requote_re = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; var d3_subclass = {}.__proto__ ? function(object, prototype) { object.__proto__ = prototype; } : function(object, prototype) { for (var property in prototype) object[property] = prototype[property]; }; function d3_selection(groups) { d3_subclass(groups, d3_selectionPrototype); return groups; } var d3_select = function(s, n) { return n.querySelector(s); }, d3_selectAll = function(s, n) { return n.querySelectorAll(s); }, d3_selectMatcher = d3_documentElement.matches || d3_documentElement[d3_vendorSymbol(d3_documentElement, "matchesSelector")], d3_selectMatches = function(n, s) { return d3_selectMatcher.call(n, s); }; if (typeof Sizzle === "function") { d3_select = function(s, n) { return Sizzle(s, n)[0] || null; }; d3_selectAll = Sizzle; d3_selectMatches = Sizzle.matchesSelector; } d3.selection = function() { return d3_selectionRoot; }; var d3_selectionPrototype = d3.selection.prototype = []; d3_selectionPrototype.select = function(selector) { var subgroups = [], subgroup, subnode, group, node; selector = d3_selection_selector(selector); for (var j = -1, m = this.length; ++j < m; ) { subgroups.push(subgroup = []); subgroup.parentNode = (group = this[j]).parentNode; for (var i = -1, n = group.length; ++i < n; ) { if (node = group[i]) { subgroup.push(subnode = selector.call(node, node.__data__, i, j)); if (subnode && "__data__" in node) subnode.__data__ = node.__data__; } else { subgroup.push(null); } } } return d3_selection(subgroups); }; function d3_selection_selector(selector) { return typeof selector === "function" ? selector : function() { return d3_select(selector, this); }; } d3_selectionPrototype.selectAll = function(selector) { var subgroups = [], subgroup, node; selector = d3_selection_selectorAll(selector); for (var j = -1, m = this.length; ++j < m; ) { for (var group = this[j], i = -1, n = group.length; ++i < n; ) { if (node = group[i]) { subgroups.push(subgroup = d3_array(selector.call(node, node.__data__, i, j))); subgroup.parentNode = node; } } } return d3_selection(subgroups); }; function d3_selection_selectorAll(selector) { return typeof selector === "function" ? selector : function() { return d3_selectAll(selector, this); }; } var d3_nsPrefix = { svg: "http://www.w3.org/2000/svg", xhtml: "http://www.w3.org/1999/xhtml", xlink: "http://www.w3.org/1999/xlink", xml: "http://www.w3.org/XML/1998/namespace", xmlns: "http://www.w3.org/2000/xmlns/" }; d3.ns = { prefix: d3_nsPrefix, qualify: function(name) { var i = name.indexOf(":"), prefix = name; if (i >= 0) { prefix = name.slice(0, i); name = name.slice(i + 1); } return d3_nsPrefix.hasOwnProperty(prefix) ? { space: d3_nsPrefix[prefix], local: name } : name; } }; d3_selectionPrototype.attr = function(name, value) { if (arguments.length < 2) { if (typeof name === "string") { var node = this.node(); name = d3.ns.qualify(name); return name.local ? node.getAttributeNS(name.space, name.local) : node.getAttribute(name); } for (value in name) this.each(d3_selection_attr(value, name[value])); return this; } return this.each(d3_selection_attr(name, value)); }; function d3_selection_attr(name, value) { name = d3.ns.qualify(name); function attrNull() { this.removeAttribute(name); } function attrNullNS() { this.removeAttributeNS(name.space, name.local); } function attrConstant() { this.setAttribute(name, value); } function attrConstantNS() { this.setAttributeNS(name.space, name.local, value); } function attrFunction() { var x = value.apply(this, arguments); if (x == null) this.removeAttribute(name); else this.setAttribute(name, x); } function attrFunctionNS() { var x = value.apply(this, arguments); if (x == null) this.removeAttributeNS(name.space, name.local); else this.setAttributeNS(name.space, name.local, x); } return value == null ? name.local ? attrNullNS : attrNull : typeof value === "function" ? name.local ? attrFunctionNS : attrFunction : name.local ? attrConstantNS : attrConstant; } function d3_collapse(s) { return s.trim().replace(/\s+/g, " "); } d3_selectionPrototype.classed = function(name, value) { if (arguments.length < 2) { if (typeof name === "string") { var node = this.node(), n = (name = d3_selection_classes(name)).length, i = -1; if (value = node.classList) { while (++i < n) if (!value.contains(name[i])) return false; } else { value = node.getAttribute("class"); while (++i < n) if (!d3_selection_classedRe(name[i]).test(value)) return false; } return true; } for (value in name) this.each(d3_selection_classed(value, name[value])); return this; } return this.each(d3_selection_classed(name, value)); }; function d3_selection_classedRe(name) { return new RegExp("(?:^|\\s+)" + d3.requote(name) + "(?:\\s+|$)", "g"); } function d3_selection_classes(name) { return (name + "").trim().split(/^|\s+/); } function d3_selection_classed(name, value) { name = d3_selection_classes(name).map(d3_selection_classedName); var n = name.length; function classedConstant() { var i = -1; while (++i < n) name[i](this, value); } function classedFunction() { var i = -1, x = value.apply(this, arguments); while (++i < n) name[i](this, x); } return typeof value === "function" ? classedFunction : classedConstant; } function d3_selection_classedName(name) { var re = d3_selection_classedRe(name); return function(node, value) { if (c = node.classList) return value ? c.add(name) : c.remove(name); var c = node.getAttribute("class") || ""; if (value) { re.lastIndex = 0; if (!re.test(c)) node.setAttribute("class", d3_collapse(c + " " + name)); } else { node.setAttribute("class", d3_collapse(c.replace(re, " "))); } }; } d3_selectionPrototype.style = function(name, value, priority) { var n = arguments.length; if (n < 3) { if (typeof name !== "string") { if (n < 2) value = ""; for (priority in name) this.each(d3_selection_style(priority, name[priority], value)); return this; } if (n < 2) return d3_window.getComputedStyle(this.node(), null).getPropertyValue(name); priority = ""; } return this.each(d3_selection_style(name, value, priority)); }; function d3_selection_style(name, value, priority) { function styleNull() { this.style.removeProperty(name); } function styleConstant() { this.style.setProperty(name, value, priority); } function styleFunction() { var x = value.apply(this, arguments); if (x == null) this.style.removeProperty(name); else this.style.setProperty(name, x, priority); } return value == null ? styleNull : typeof value === "function" ? styleFunction : styleConstant; } d3_selectionPrototype.property = function(name, value) { if (arguments.length < 2) { if (typeof name === "string") return this.node()[name]; for (value in name) this.each(d3_selection_property(value, name[value])); return this; } return this.each(d3_selection_property(name, value)); }; function d3_selection_property(name, value) { function propertyNull() { delete this[name]; } function propertyConstant() { this[name] = value; } function propertyFunction() { var x = value.apply(this, arguments); if (x == null) delete this[name]; else this[name] = x; } return value == null ? propertyNull : typeof value === "function" ? propertyFunction : propertyConstant; } d3_selectionPrototype.text = function(value) { return arguments.length ? this.each(typeof value === "function" ? function() { var v = value.apply(this, arguments); this.textContent = v == null ? "" : v; } : value == null ? function() { this.textContent = ""; } : function() { this.textContent = value; }) : this.node().textContent; }; d3_selectionPrototype.html = function(value) { return arguments.length ? this.each(typeof value === "function" ? function() { var v = value.apply(this, arguments); this.innerHTML = v == null ? "" : v; } : value == null ? function() { this.innerHTML = ""; } : function() { this.innerHTML = value; }) : this.node().innerHTML; }; d3_selectionPrototype.append = function(name) { name = d3_selection_creator(name); return this.select(function() { return this.appendChild(name.apply(this, arguments)); }); }; function d3_selection_creator(name) { return typeof name === "function" ? name : (name = d3.ns.qualify(name)).local ? function() { return this.ownerDocument.createElementNS(name.space, name.local); } : function() { return this.ownerDocument.createElementNS(this.namespaceURI, name); }; } d3_selectionPrototype.insert = function(name, before) { name = d3_selection_creator(name); before = d3_selection_selector(before); return this.select(function() { return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null); }); }; d3_selectionPrototype.remove = function() { return this.each(d3_selectionRemove); }; function d3_selectionRemove() { var parent = this.parentNode; if (parent) parent.removeChild(this); } d3_selectionPrototype.data = function(value, key) { var i = -1, n = this.length, group, node; if (!arguments.length) { value = new Array(n = (group = this[0]).length); while (++i < n) { if (node = group[i]) { value[i] = node.__data__; } } return value; } function bind(group, groupData) { var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData; if (key) { var nodeByKeyValue = new d3_Map(), keyValues = new Array(n), keyValue; for (i = -1; ++i < n; ) { if (nodeByKeyValue.has(keyValue = key.call(node = group[i], node.__data__, i))) { exitNodes[i] = node; } else { nodeByKeyValue.set(keyValue, node); } keyValues[i] = keyValue; } for (i = -1; ++i < m; ) { if (!(node = nodeByKeyValue.get(keyValue = key.call(groupData, nodeData = groupData[i], i)))) { enterNodes[i] = d3_selection_dataNode(nodeData); } else if (node !== true) { updateNodes[i] = node; node.__data__ = nodeData; } nodeByKeyValue.set(keyValue, true); } for (i = -1; ++i < n; ) { if (nodeByKeyValue.get(keyValues[i]) !== true) { exitNodes[i] = group[i]; } } } else { for (i = -1; ++i < n0; ) { node = group[i]; nodeData = groupData[i]; if (node) { node.__data__ = nodeData; updateNodes[i] = node; } else { enterNodes[i] = d3_selection_dataNode(nodeData); } } for (;i < m; ++i) { enterNodes[i] = d3_selection_dataNode(groupData[i]); } for (;i < n; ++i) { exitNodes[i] = group[i]; } } enterNodes.update = updateNodes; enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode; enter.push(enterNodes); update.push(updateNodes); exit.push(exitNodes); } var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]); if (typeof value === "function") { while (++i < n) { bind(group = this[i], value.call(group, group.parentNode.__data__, i)); } } else { while (++i < n) { bind(group = this[i], value); } } update.enter = function() { return enter; }; update.exit = function() { return exit; }; return update; }; function d3_selection_dataNode(data) { return { __data__: data }; } d3_selectionPrototype.datum = function(value) { return arguments.length ? this.property("__data__", value) : this.property("__data__"); }; d3_selectionPrototype.filter = function(filter) { var subgroups = [], subgroup, group, node; if (typeof filter !== "function") filter = d3_selection_filter(filter); for (var j = 0, m = this.length; j < m; j++) { subgroups.push(subgroup = []); subgroup.parentNode = (group = this[j]).parentNode; for (var i = 0, n = group.length; i < n; i++) { if ((node = group[i]) && filter.call(node, node.__data__, i, j)) { subgroup.push(node); } } } return d3_selection(subgroups); }; function d3_selection_filter(selector) { return function() { return d3_selectMatches(this, selector); }; } d3_selectionPrototype.order = function() { for (var j = -1, m = this.length; ++j < m; ) { for (var group = this[j], i = group.length - 1, next = group[i], node; --i >= 0; ) { if (node = group[i]) { if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next); next = node; } } } return this; }; d3_selectionPrototype.sort = function(comparator) { comparator = d3_selection_sortComparator.apply(this, arguments); for (var j = -1, m = this.length; ++j < m; ) this[j].sort(comparator); return this.order(); }; function d3_selection_sortComparator(comparator) { if (!arguments.length) comparator = d3_ascending; return function(a, b) { return a && b ? comparator(a.__data__, b.__data__) : !a - !b; }; } d3_selectionPrototype.each = function(callback) { return d3_selection_each(this, function(node, i, j) { callback.call(node, node.__data__, i, j); }); }; function d3_selection_each(groups, callback) { for (var j = 0, m = groups.length; j < m; j++) { for (var group = groups[j], i = 0, n = group.length, node; i < n; i++) { if (node = group[i]) callback(node, i, j); } } return groups; } d3_selectionPrototype.call = function(callback) { var args = d3_array(arguments); callback.apply(args[0] = this, args); return this; }; d3_selectionPrototype.empty = function() { return !this.node(); }; d3_selectionPrototype.node = function() { for (var j = 0, m = this.length; j < m; j++) { for (var group = this[j], i = 0, n = group.length; i < n; i++) { var node = group[i]; if (node) return node; } } return null; }; d3_selectionPrototype.size = function() { var n = 0; d3_selection_each(this, function() { ++n; }); return n; }; function d3_selection_enter(selection) { d3_subclass(selection, d3_selection_enterPrototype); return selection; } var d3_selection_enterPrototype = []; d3.selection.enter = d3_selection_enter; d3.selection.enter.prototype = d3_selection_enterPrototype; d3_selection_enterPrototype.append = d3_selectionPrototype.append; d3_selection_enterPrototype.empty = d3_selectionPrototype.empty; d3_selection_enterPrototype.node = d3_selectionPrototype.node; d3_selection_enterPrototype.call = d3_selectionPrototype.call; d3_selection_enterPrototype.size = d3_selectionPrototype.size; d3_selection_enterPrototype.select = function(selector) { var subgroups = [], subgroup, subnode, upgroup, group, node; for (var j = -1, m = this.length; ++j < m; ) { upgroup = (group = this[j]).update; subgroups.push(subgroup = []); subgroup.parentNode = group.parentNode; for (var i = -1, n = group.length; ++i < n; ) { if (node = group[i]) { subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i, j)); subnode.__data__ = node.__data__; } else { subgroup.push(null); } } } return d3_selection(subgroups); }; d3_selection_enterPrototype.insert = function(name, before) { if (arguments.length < 2) before = d3_selection_enterInsertBefore(this); return d3_selectionPrototype.insert.call(this, name, before); }; function d3_selection_enterInsertBefore(enter) { var i0, j0; return function(d, i, j) { var group = enter[j].update, n = group.length, node; if (j != j0) j0 = j, i0 = 0; if (i >= i0) i0 = i + 1; while (!(node = group[i0]) && ++i0 < n) ; return node; }; } d3.select = function(node) { var group = [ typeof node === "string" ? d3_select(node, d3_document) : node ]; group.parentNode = d3_documentElement; return d3_selection([ group ]); }; d3.selectAll = function(nodes) { var group = d3_array(typeof nodes === "string" ? d3_selectAll(nodes, d3_document) : nodes); group.parentNode = d3_documentElement; return d3_selection([ group ]); }; var d3_selectionRoot = d3.select(d3_documentElement); d3_selectionPrototype.on = function(type, listener, capture) { var n = arguments.length; if (n < 3) { if (typeof type !== "string") { if (n < 2) listener = false; for (capture in type) this.each(d3_selection_on(capture, type[capture], listener)); return this; } if (n < 2) return (n = this.node()["__on" + type]) && n._; capture = false; } return this.each(d3_selection_on(type, listener, capture)); }; function d3_selection_on(type, listener, capture) { var name = "__on" + type, i = type.indexOf("."), wrap = d3_selection_onListener; if (i > 0) type = type.slice(0, i); var filter = d3_selection_onFilters.get(type); if (filter) type = filter, wrap = d3_selection_onFilter; function onRemove() { var l = this[name]; if (l) { this.removeEventListener(type, l, l.$); delete this[name]; } } function onAdd() { var l = wrap(listener, d3_array(arguments)); onRemove.call(this); this.addEventListener(type, this[name] = l, l.$ = capture); l._ = listener; } function removeAll() { var re = new RegExp("^__on([^.]+)" + d3.requote(type) + "$"), match; for (var name in this) { if (match = name.match(re)) { var l = this[name]; this.removeEventListener(match[1], l, l.$); delete this[name]; } } } return i ? listener ? onAdd : onRemove : listener ? d3_noop : removeAll; } var d3_selection_onFilters = d3.map({ mouseenter: "mouseover", mouseleave: "mouseout" }); d3_selection_onFilters.forEach(function(k) { if ("on" + k in d3_document) d3_selection_onFilters.remove(k); }); function d3_selection_onListener(listener, argumentz) { return function(e) { var o = d3.event; d3.event = e; argumentz[0] = this.__data__; try { listener.apply(this, argumentz); } finally { d3.event = o; } }; } function d3_selection_onFilter(listener, argumentz) { var l = d3_selection_onListener(listener, argumentz); return function(e) { var target = this, related = e.relatedTarget; if (!related || related !== target && !(related.compareDocumentPosition(target) & 8)) { l.call(target, e); } }; } var d3_event_dragSelect = "onselectstart" in d3_document ? null : d3_vendorSymbol(d3_documentElement.style, "userSelect"), d3_event_dragId = 0; function d3_event_dragSuppress() { var name = ".dragsuppress-" + ++d3_event_dragId, click = "click" + name, w = d3.select(d3_window).on("touchmove" + name, d3_eventPreventDefault).on("dragstart" + name, d3_eventPreventDefault).on("selectstart" + name, d3_eventPreventDefault); if (d3_event_dragSelect) { var style = d3_documentElement.style, select = style[d3_event_dragSelect]; style[d3_event_dragSelect] = "none"; } return function(suppressClick) { w.on(name, null); if (d3_event_dragSelect) style[d3_event_dragSelect] = select; if (suppressClick) { var off = function() { w.on(click, null); }; w.on(click, function() { d3_eventPreventDefault(); off(); }, true); setTimeout(off, 0); } }; } d3.mouse = function(container) { return d3_mousePoint(container, d3_eventSource()); }; var d3_mouse_bug44083 = /WebKit/.test(d3_window.navigator.userAgent) ? -1 : 0; function d3_mousePoint(container, e) { if (e.changedTouches) e = e.changedTouches[0]; var svg = container.ownerSVGElement || container; if (svg.createSVGPoint) { var point = svg.createSVGPoint(); if (d3_mouse_bug44083 < 0 && (d3_window.scrollX || d3_window.scrollY)) { svg = d3.select("body").append("svg").style({ position: "absolute", top: 0, left: 0, margin: 0, padding: 0, border: "none" }, "important"); var ctm = svg[0][0].getScreenCTM(); d3_mouse_bug44083 = !(ctm.f || ctm.e); svg.remove(); } if (d3_mouse_bug44083) point.x = e.pageX, point.y = e.pageY; else point.x = e.clientX, point.y = e.clientY; point = point.matrixTransform(container.getScreenCTM().inverse()); return [ point.x, point.y ]; } var rect = container.getBoundingClientRect(); return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ]; } d3.touch = function(container, touches, identifier) { if (arguments.length < 3) identifier = touches, touches = d3_eventSource().changedTouches; if (touches) for (var i = 0, n = touches.length, touch; i < n; ++i) { if ((touch = touches[i]).identifier === identifier) { return d3_mousePoint(container, touch); } } }; d3.behavior.drag = function() { var event = d3_eventDispatch(drag, "drag", "dragstart", "dragend"), origin = null, mousedown = dragstart(d3_noop, d3.mouse, d3_behavior_dragMouseSubject, "mousemove", "mouseup"), touchstart = dragstart(d3_behavior_dragTouchId, d3.touch, d3_behavior_dragTouchSubject, "touchmove", "touchend"); function drag() { this.on("mousedown.drag", mousedown).on("touchstart.drag", touchstart); } function dragstart(id, position, subject, move, end) { return function() { var that = this, target = d3.event.target, parent = that.parentNode, dispatch = event.of(that, arguments), dragged = 0, dragId = id(), dragName = ".drag" + (dragId == null ? "" : "-" + dragId), dragOffset, dragSubject = d3.select(subject()).on(move + dragName, moved).on(end + dragName, ended), dragRestore = d3_event_dragSuppress(), position0 = position(parent, dragId); if (origin) { dragOffset = origin.apply(that, arguments); dragOffset = [ dragOffset.x - position0[0], dragOffset.y - position0[1] ]; } else { dragOffset = [ 0, 0 ]; } dispatch({ type: "dragstart" }); function moved() { var position1 = position(parent, dragId), dx, dy; if (!position1) return; dx = position1[0] - position0[0]; dy = position1[1] - position0[1]; dragged |= dx | dy; position0 = position1; dispatch({ type: "drag", x: position1[0] + dragOffset[0], y: position1[1] + dragOffset[1], dx: dx, dy: dy }); } function ended() { if (!position(parent, dragId)) return; dragSubject.on(move + dragName, null).on(end + dragName, null); dragRestore(dragged && d3.event.target === target); dispatch({ type: "dragend" }); } }; } drag.origin = function(x) { if (!arguments.length) return origin; origin = x; return drag; }; return d3.rebind(drag, event, "on"); }; function d3_behavior_dragTouchId() { return d3.event.changedTouches[0].identifier; } function d3_behavior_dragTouchSubject() { return d3.event.target; } function d3_behavior_dragMouseSubject() { return d3_window; } d3.touches = function(container, touches) { if (arguments.length < 2) touches = d3_eventSource().touches; return touches ? d3_array(touches).map(function(touch) { var point = d3_mousePoint(container, touch); point.identifier = touch.identifier; return point; }) : []; }; var ε = 1e-6, ε2 = ε * ε, π = Math.PI, τ = 2 * π, τε = τ - ε, halfπ = π / 2, d3_radians = π / 180, d3_degrees = 180 / π; function d3_sgn(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; } function d3_cross2d(a, b, c) { return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); } function d3_acos(x) { return x > 1 ? 0 : x < -1 ? π : Math.acos(x); } function d3_asin(x) { return x > 1 ? halfπ : x < -1 ? -halfπ : Math.asin(x); } function d3_sinh(x) { return ((x = Math.exp(x)) - 1 / x) / 2; } function d3_cosh(x) { return ((x = Math.exp(x)) + 1 / x) / 2; } function d3_tanh(x) { return ((x = Math.exp(2 * x)) - 1) / (x + 1); } function d3_haversin(x) { return (x = Math.sin(x / 2)) * x; } var ρ = Math.SQRT2, ρ2 = 2, ρ4 = 4; d3.interpolateZoom = function(p0, p1) { var ux0 = p0[0], uy0 = p0[1], w0 = p0[2], ux1 = p1[0], uy1 = p1[1], w1 = p1[2]; var dx = ux1 - ux0, dy = uy1 - uy0, d2 = dx * dx + dy * dy, d1 = Math.sqrt(d2), b0 = (w1 * w1 - w0 * w0 + ρ4 * d2) / (2 * w0 * ρ2 * d1), b1 = (w1 * w1 - w0 * w0 - ρ4 * d2) / (2 * w1 * ρ2 * d1), r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0), r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1), dr = r1 - r0, S = (dr || Math.log(w1 / w0)) / ρ; function interpolate(t) { var s = t * S; if (dr) { var coshr0 = d3_cosh(r0), u = w0 / (ρ2 * d1) * (coshr0 * d3_tanh(ρ * s + r0) - d3_sinh(r0)); return [ ux0 + u * dx, uy0 + u * dy, w0 * coshr0 / d3_cosh(ρ * s + r0) ]; } return [ ux0 + t * dx, uy0 + t * dy, w0 * Math.exp(ρ * s) ]; } interpolate.duration = S * 1e3; return interpolate; }; d3.behavior.zoom = function() { var view = { x: 0, y: 0, k: 1 }, translate0, center0, center, size = [ 960, 500 ], scaleExtent = d3_behavior_zoomInfinity, duration = 250, zooming = 0, mousedown = "mousedown.zoom", mousemove = "mousemove.zoom", mouseup = "mouseup.zoom", mousewheelTimer, touchstart = "touchstart.zoom", touchtime, event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"), x0, x1, y0, y1; function zoom(g) { g.on(mousedown, mousedowned).on(d3_behavior_zoomWheel + ".zoom", mousewheeled).on("dblclick.zoom", dblclicked).on(touchstart, touchstarted); } zoom.event = function(g) { g.each(function() { var dispatch = event.of(this, arguments), view1 = view; if (d3_transitionInheritId) { d3.select(this).transition().each("start.zoom", function() { view = this.__chart__ || { x: 0, y: 0, k: 1 }; zoomstarted(dispatch); }).tween("zoom:zoom", function() { var dx = size[0], dy = size[1], cx = center0 ? center0[0] : dx / 2, cy = center0 ? center0[1] : dy / 2, i = d3.interpolateZoom([ (cx - view.x) / view.k, (cy - view.y) / view.k, dx / view.k ], [ (cx - view1.x) / view1.k, (cy - view1.y) / view1.k, dx / view1.k ]); return function(t) { var l = i(t), k = dx / l[2]; this.__chart__ = view = { x: cx - l[0] * k, y: cy - l[1] * k, k: k }; zoomed(dispatch); }; }).each("interrupt.zoom", function() { zoomended(dispatch); }).each("end.zoom", function() { zoomended(dispatch); }); } else { this.__chart__ = view; zoomstarted(dispatch); zoomed(dispatch); zoomended(dispatch); } }); }; zoom.translate = function(_) { if (!arguments.length) return [ view.x, view.y ]; view = { x: +_[0], y: +_[1], k: view.k }; rescale(); return zoom; }; zoom.scale = function(_) { if (!arguments.length) return view.k; view = { x: view.x, y: view.y, k: +_ }; rescale(); return zoom; }; zoom.scaleExtent = function(_) { if (!arguments.length) return scaleExtent; scaleExtent = _ == null ? d3_behavior_zoomInfinity : [ +_[0], +_[1] ]; return zoom; }; zoom.center = function(_) { if (!arguments.length) return center; center = _ && [ +_[0], +_[1] ]; return zoom; }; zoom.size = function(_) { if (!arguments.length) return size; size = _ && [ +_[0], +_[1] ]; return zoom; }; zoom.duration = function(_) { if (!arguments.length) return duration; duration = +_; return zoom; }; zoom.x = function(z) { if (!arguments.length) return x1; x1 = z; x0 = z.copy(); view = { x: 0, y: 0, k: 1 }; return zoom; }; zoom.y = function(z) { if (!arguments.length) return y1; y1 = z; y0 = z.copy(); view = { x: 0, y: 0, k: 1 }; return zoom; }; function location(p) { return [ (p[0] - view.x) / view.k, (p[1] - view.y) / view.k ]; } function point(l) { return [ l[0] * view.k + view.x, l[1] * view.k + view.y ]; } function scaleTo(s) { view.k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], s)); } function translateTo(p, l) { l = point(l); view.x += p[0] - l[0]; view.y += p[1] - l[1]; } function zoomTo(that, p, l, k) { that.__chart__ = { x: view.x, y: view.y, k: view.k }; scaleTo(Math.pow(2, k)); translateTo(center0 = p, l); that = d3.select(that); if (duration > 0) that = that.transition().duration(duration); that.call(zoom.event); } function rescale() { if (x1) x1.domain(x0.range().map(function(x) { return (x - view.x) / view.k; }).map(x0.invert)); if (y1) y1.domain(y0.range().map(function(y) { return (y - view.y) / view.k; }).map(y0.invert)); } function zoomstarted(dispatch) { if (!zooming++) dispatch({ type: "zoomstart" }); } function zoomed(dispatch) { rescale(); dispatch({ type: "zoom", scale: view.k, translate: [ view.x, view.y ] }); } function zoomended(dispatch) { if (!--zooming) dispatch({ type: "zoomend" }); center0 = null; } function mousedowned() { var that = this, target = d3.event.target, dispatch = event.of(that, arguments), dragged = 0, subject = d3.select(d3_window).on(mousemove, moved).on(mouseup, ended), location0 = location(d3.mouse(that)), dragRestore = d3_event_dragSuppress(); d3_selection_interrupt.call(that); zoomstarted(dispatch); function moved() { dragged = 1; translateTo(d3.mouse(that), location0); zoomed(dispatch); } function ended() { subject.on(mousemove, null).on(mouseup, null); dragRestore(dragged && d3.event.target === target); zoomended(dispatch); } } function touchstarted() { var that = this, dispatch = event.of(that, arguments), locations0 = {}, distance0 = 0, scale0, zoomName = ".zoom-" + d3.event.changedTouches[0].identifier, touchmove = "touchmove" + zoomName, touchend = "touchend" + zoomName, targets = [], subject = d3.select(that), dragRestore = d3_event_dragSuppress(); started(); zoomstarted(dispatch); subject.on(mousedown, null).on(touchstart, started); function relocate() { var touches = d3.touches(that); scale0 = view.k; touches.forEach(function(t) { if (t.identifier in locations0) locations0[t.identifier] = location(t); }); return touches; } function started() { var target = d3.event.target; d3.select(target).on(touchmove, moved).on(touchend, ended); targets.push(target); var changed = d3.event.changedTouches; for (var i = 0, n = changed.length; i < n; ++i) { locations0[changed[i].identifier] = null; } var touches = relocate(), now = Date.now(); if (touches.length === 1) { if (now - touchtime < 500) { var p = touches[0]; zoomTo(that, p, locations0[p.identifier], Math.floor(Math.log(view.k) / Math.LN2) + 1); d3_eventPreventDefault(); } touchtime = now; } else if (touches.length > 1) { var p = touches[0], q = touches[1], dx = p[0] - q[0], dy = p[1] - q[1]; distance0 = dx * dx + dy * dy; } } function moved() { var touches = d3.touches(that), p0, l0, p1, l1; d3_selection_interrupt.call(that); for (var i = 0, n = touches.length; i < n; ++i, l1 = null) { p1 = touches[i]; if (l1 = locations0[p1.identifier]) { if (l0) break; p0 = p1, l0 = l1; } } if (l1) { var distance1 = (distance1 = p1[0] - p0[0]) * distance1 + (distance1 = p1[1] - p0[1]) * distance1, scale1 = distance0 && Math.sqrt(distance1 / distance0); p0 = [ (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2 ]; l0 = [ (l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2 ]; scaleTo(scale1 * scale0); } touchtime = null; translateTo(p0, l0); zoomed(dispatch); } function ended() { if (d3.event.touches.length) { var changed = d3.event.changedTouches; for (var i = 0, n = changed.length; i < n; ++i) { delete locations0[changed[i].identifier]; } for (var identifier in locations0) { return void relocate(); } } d3.selectAll(targets).on(zoomName, null); subject.on(mousedown, mousedowned).on(touchstart, touchstarted); dragRestore(); zoomended(dispatch); } } function mousewheeled() { var dispatch = event.of(this, arguments); if (mousewheelTimer) clearTimeout(mousewheelTimer); else translate0 = location(center0 = center || d3.mouse(this)), d3_selection_interrupt.call(this), zoomstarted(dispatch); mousewheelTimer = setTimeout(function() { mousewheelTimer = null; zoomended(dispatch); }, 50); d3_eventPreventDefault(); scaleTo(Math.pow(2, d3_behavior_zoomDelta() * .002) * view.k); translateTo(center0, translate0); zoomed(dispatch); } function dblclicked() { var p = d3.mouse(this), k = Math.log(view.k) / Math.LN2; zoomTo(this, p, location(p), d3.event.shiftKey ? Math.ceil(k) - 1 : Math.floor(k) + 1); } return d3.rebind(zoom, event, "on"); }; var d3_behavior_zoomInfinity = [ 0, Infinity ]; var d3_behavior_zoomDelta, d3_behavior_zoomWheel = "onwheel" in d3_document ? (d3_behavior_zoomDelta = function() { return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1); }, "wheel") : "onmousewheel" in d3_document ? (d3_behavior_zoomDelta = function() { return d3.event.wheelDelta; }, "mousewheel") : (d3_behavior_zoomDelta = function() { return -d3.event.detail; }, "MozMousePixelScroll"); d3.color = d3_color; function d3_color() {} d3_color.prototype.toString = function() { return this.rgb() + ""; }; d3.hsl = d3_hsl; function d3_hsl(h, s, l) { return this instanceof d3_hsl ? void (this.h = +h, this.s = +s, this.l = +l) : arguments.length < 2 ? h instanceof d3_hsl ? new d3_hsl(h.h, h.s, h.l) : d3_rgb_parse("" + h, d3_rgb_hsl, d3_hsl) : new d3_hsl(h, s, l); } var d3_hslPrototype = d3_hsl.prototype = new d3_color(); d3_hslPrototype.brighter = function(k) { k = Math.pow(.7, arguments.length ? k : 1); return new d3_hsl(this.h, this.s, this.l / k); }; d3_hslPrototype.darker = function(k) { k = Math.pow(.7, arguments.length ? k : 1); return new d3_hsl(this.h, this.s, k * this.l); }; d3_hslPrototype.rgb = function() { return d3_hsl_rgb(this.h, this.s, this.l); }; function d3_hsl_rgb(h, s, l) { var m1, m2; h = isNaN(h) ? 0 : (h %= 360) < 0 ? h + 360 : h; s = isNaN(s) ? 0 : s < 0 ? 0 : s > 1 ? 1 : s; l = l < 0 ? 0 : l > 1 ? 1 : l; m2 = l <= .5 ? l * (1 + s) : l + s - l * s; m1 = 2 * l - m2; function v(h) { if (h > 360) h -= 360; else if (h < 0) h += 360; if (h < 60) return m1 + (m2 - m1) * h / 60; if (h < 180) return m2; if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60; return m1; } function vv(h) { return Math.round(v(h) * 255); } return new d3_rgb(vv(h + 120), vv(h), vv(h - 120)); } d3.hcl = d3_hcl; function d3_hcl(h, c, l) { return this instanceof d3_hcl ? void (this.h = +h, this.c = +c, this.l = +l) : arguments.length < 2 ? h instanceof d3_hcl ? new d3_hcl(h.h, h.c, h.l) : h instanceof d3_lab ? d3_lab_hcl(h.l, h.a, h.b) : d3_lab_hcl((h = d3_rgb_lab((h = d3.rgb(h)).r, h.g, h.b)).l, h.a, h.b) : new d3_hcl(h, c, l); } var d3_hclPrototype = d3_hcl.prototype = new d3_color(); d3_hclPrototype.brighter = function(k) { return new d3_hcl(this.h, this.c, Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1))); }; d3_hclPrototype.darker = function(k) { return new d3_hcl(this.h, this.c, Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1))); }; d3_hclPrototype.rgb = function() { return d3_hcl_lab(this.h, this.c, this.l).rgb(); }; function d3_hcl_lab(h, c, l) { if (isNaN(h)) h = 0; if (isNaN(c)) c = 0; return new d3_lab(l, Math.cos(h *= d3_radians) * c, Math.sin(h) * c); } d3.lab = d3_lab; function d3_lab(l, a, b) { return this instanceof d3_lab ? void (this.l = +l, this.a = +a, this.b = +b) : arguments.length < 2 ? l instanceof d3_lab ? new d3_lab(l.l, l.a, l.b) : l instanceof d3_hcl ? d3_hcl_lab(l.h, l.c, l.l) : d3_rgb_lab((l = d3_rgb(l)).r, l.g, l.b) : new d3_lab(l, a, b); } var d3_lab_K = 18; var d3_lab_X = .95047, d3_lab_Y = 1, d3_lab_Z = 1.08883; var d3_labPrototype = d3_lab.prototype = new d3_color(); d3_labPrototype.brighter = function(k) { return new d3_lab(Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)), this.a, this.b); }; d3_labPrototype.darker = function(k) { return new d3_lab(Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)), this.a, this.b); }; d3_labPrototype.rgb = function() { return d3_lab_rgb(this.l, this.a, this.b); }; function d3_lab_rgb(l, a, b) { var y = (l + 16) / 116, x = y + a / 500, z = y - b / 200; x = d3_lab_xyz(x) * d3_lab_X; y = d3_lab_xyz(y) * d3_lab_Y; z = d3_lab_xyz(z) * d3_lab_Z; return new d3_rgb(d3_xyz_rgb(3.2404542 * x - 1.5371385 * y - .4985314 * z), d3_xyz_rgb(-.969266 * x + 1.8760108 * y + .041556 * z), d3_xyz_rgb(.0556434 * x - .2040259 * y + 1.0572252 * z)); } function d3_lab_hcl(l, a, b) { return l > 0 ? new d3_hcl(Math.atan2(b, a) * d3_degrees, Math.sqrt(a * a + b * b), l) : new d3_hcl(NaN, NaN, l); } function d3_lab_xyz(x) { return x > .206893034 ? x * x * x : (x - 4 / 29) / 7.787037; } function d3_xyz_lab(x) { return x > .008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29; } function d3_xyz_rgb(r) { return Math.round(255 * (r <= .00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - .055)); } d3.rgb = d3_rgb; function d3_rgb(r, g, b) { return this instanceof d3_rgb ? void (this.r = ~~r, this.g = ~~g, this.b = ~~b) : arguments.length < 2 ? r instanceof d3_rgb ? new d3_rgb(r.r, r.g, r.b) : d3_rgb_parse("" + r, d3_rgb, d3_hsl_rgb) : new d3_rgb(r, g, b); } function d3_rgbNumber(value) { return new d3_rgb(value >> 16, value >> 8 & 255, value & 255); } function d3_rgbString(value) { return d3_rgbNumber(value) + ""; } var d3_rgbPrototype = d3_rgb.prototype = new d3_color(); d3_rgbPrototype.brighter = function(k) { k = Math.pow(.7, arguments.length ? k : 1); var r = this.r, g = this.g, b = this.b, i = 30; if (!r && !g && !b) return new d3_rgb(i, i, i); if (r && r < i) r = i; if (g && g < i) g = i; if (b && b < i) b = i; return new d3_rgb(Math.min(255, r / k), Math.min(255, g / k), Math.min(255, b / k)); }; d3_rgbPrototype.darker = function(k) { k = Math.pow(.7, arguments.length ? k : 1); return new d3_rgb(k * this.r, k * this.g, k * this.b); }; d3_rgbPrototype.hsl = function() { return d3_rgb_hsl(this.r, this.g, this.b); }; d3_rgbPrototype.toString = function() { return "#" + d3_rgb_hex(this.r) + d3_rgb_hex(this.g) + d3_rgb_hex(this.b); }; function d3_rgb_hex(v) { return v < 16 ? "0" + Math.max(0, v).toString(16) : Math.min(255, v).toString(16); } function d3_rgb_parse(format, rgb, hsl) { var r = 0, g = 0, b = 0, m1, m2, color; m1 = /([a-z]+)\((.*)\)/i.exec(format); if (m1) { m2 = m1[2].split(","); switch (m1[1]) { case "hsl": { return hsl(parseFloat(m2[0]), parseFloat(m2[1]) / 100, parseFloat(m2[2]) / 100); } case "rgb": { return rgb(d3_rgb_parseNumber(m2[0]), d3_rgb_parseNumber(m2[1]), d3_rgb_parseNumber(m2[2])); } } } if (color = d3_rgb_names.get(format)) return rgb(color.r, color.g, color.b); if (format != null && format.charAt(0) === "#" && !isNaN(color = parseInt(format.slice(1), 16))) { if (format.length === 4) { r = (color & 3840) >> 4; r = r >> 4 | r; g = color & 240; g = g >> 4 | g; b = color & 15; b = b << 4 | b; } else if (format.length === 7) { r = (color & 16711680) >> 16; g = (color & 65280) >> 8; b = color & 255; } } return rgb(r, g, b); } function d3_rgb_hsl(r, g, b) { var min = Math.min(r /= 255, g /= 255, b /= 255), max = Math.max(r, g, b), d = max - min, h, s, l = (max + min) / 2; if (d) { s = l < .5 ? d / (max + min) : d / (2 - max - min); if (r == max) h = (g - b) / d + (g < b ? 6 : 0); else if (g == max) h = (b - r) / d + 2; else h = (r - g) / d + 4; h *= 60; } else { h = NaN; s = l > 0 && l < 1 ? 0 : h; } return new d3_hsl(h, s, l); } function d3_rgb_lab(r, g, b) { r = d3_rgb_xyz(r); g = d3_rgb_xyz(g); b = d3_rgb_xyz(b); var x = d3_xyz_lab((.4124564 * r + .3575761 * g + .1804375 * b) / d3_lab_X), y = d3_xyz_lab((.2126729 * r + .7151522 * g + .072175 * b) / d3_lab_Y), z = d3_xyz_lab((.0193339 * r + .119192 * g + .9503041 * b) / d3_lab_Z); return d3_lab(116 * y - 16, 500 * (x - y), 200 * (y - z)); } function d3_rgb_xyz(r) { return (r /= 255) <= .04045 ? r / 12.92 : Math.pow((r + .055) / 1.055, 2.4); } function d3_rgb_parseNumber(c) { var f = parseFloat(c); return c.charAt(c.length - 1) === "%" ? Math.round(f * 2.55) : f; } var d3_rgb_names = d3.map({ aliceblue: 15792383, antiquewhite: 16444375, aqua: 65535, aquamarine: 8388564, azure: 15794175, beige: 16119260, bisque: 16770244, black: 0, blanchedalmond: 16772045, blue: 255, blueviolet: 9055202, brown: 10824234, burlywood: 14596231, cadetblue: 6266528, chartreuse: 8388352, chocolate: 13789470, coral: 16744272, cornflowerblue: 6591981, cornsilk: 16775388, crimson: 14423100, cyan: 65535, darkblue: 139, darkcyan: 35723, darkgoldenrod: 12092939, darkgray: 11119017, darkgreen: 25600, darkgrey: 11119017, darkkhaki: 12433259, darkmagenta: 9109643, darkolivegreen: 5597999, darkorange: 16747520, darkorchid: 10040012, darkred: 9109504, darksalmon: 15308410, darkseagreen: 9419919, darkslateblue: 4734347, darkslategray: 3100495, darkslategrey: 3100495, darkturquoise: 52945, darkviolet: 9699539, deeppink: 16716947, deepskyblue: 49151, dimgray: 6908265, dimgrey: 6908265, dodgerblue: 2003199, firebrick: 11674146, floralwhite: 16775920, forestgreen: 2263842, fuchsia: 16711935, gainsboro: 14474460, ghostwhite: 16316671, gold: 16766720, goldenrod: 14329120, gray: 8421504, green: 32768, greenyellow: 11403055, grey: 8421504, honeydew: 15794160, hotpink: 16738740, indianred: 13458524, indigo: 4915330, ivory: 16777200, khaki: 15787660, lavender: 15132410, lavenderblush: 16773365, lawngreen: 8190976, lemonchiffon: 16775885, lightblue: 11393254, lightcoral: 15761536, lightcyan: 14745599, lightgoldenrodyellow: 16448210, lightgray: 13882323, lightgreen: 9498256, lightgrey: 13882323, lightpink: 16758465, lightsalmon: 16752762, lightseagreen: 2142890, lightskyblue: 8900346, lightslategray: 7833753, lightslategrey: 7833753, lightsteelblue: 11584734, lightyellow: 16777184, lime: 65280, limegreen: 3329330, linen: 16445670, magenta: 16711935, maroon: 8388608, mediumaquamarine: 6737322, mediumblue: 205, mediumorchid: 12211667, mediumpurple: 9662683, mediumseagreen: 3978097, mediumslateblue: 8087790, mediumspringgreen: 64154, mediumturquoise: 4772300, mediumvioletred: 13047173, midnightblue: 1644912, mintcream: 16121850, mistyrose: 16770273, moccasin: 16770229, navajowhite: 16768685, navy: 128, oldlace: 16643558, olive: 8421376, olivedrab: 7048739, orange: 16753920, orangered: 16729344, orchid: 14315734, palegoldenrod: 15657130, palegreen: 10025880, paleturquoise: 11529966, palevioletred: 14381203, papayawhip: 16773077, peachpuff: 16767673, peru: 13468991, pink: 16761035, plum: 14524637, powderblue: 11591910, purple: 8388736, red: 16711680, rosybrown: 12357519, royalblue: 4286945, saddlebrown: 9127187, salmon: 16416882, sandybrown: 16032864, seagreen: 3050327, seashell: 16774638, sienna: 10506797, silver: 12632256, skyblue: 8900331, slateblue: 6970061, slategray: 7372944, slategrey: 7372944, snow: 16775930, springgreen: 65407, steelblue: 4620980, tan: 13808780, teal: 32896, thistle: 14204888, tomato: 16737095, turquoise: 4251856, violet: 15631086, wheat: 16113331, white: 16777215, whitesmoke: 16119285, yellow: 16776960, yellowgreen: 10145074 }); d3_rgb_names.forEach(function(key, value) { d3_rgb_names.set(key, d3_rgbNumber(value)); }); function d3_functor(v) { return typeof v === "function" ? v : function() { return v; }; } d3.functor = d3_functor; function d3_identity(d) { return d; } d3.xhr = d3_xhrType(d3_identity); function d3_xhrType(response) { return function(url, mimeType, callback) { if (arguments.length === 2 && typeof mimeType === "function") callback = mimeType, mimeType = null; return d3_xhr(url, mimeType, response, callback); }; } function d3_xhr(url, mimeType, response, callback) { var xhr = {}, dispatch = d3.dispatch("beforesend", "progress", "load", "error"), headers = {}, request = new XMLHttpRequest(), responseType = null; if (d3_window.XDomainRequest && !("withCredentials" in request) && /^(http(s)?:)?\/\//.test(url)) request = new XDomainRequest(); "onload" in request ? request.onload = request.onerror = respond : request.onreadystatechange = function() { request.readyState > 3 && respond(); }; function respond() { var status = request.status, result; if (!status && d3_xhrHasResponse(request) || status >= 200 && status < 300 || status === 304) { try { result = response.call(xhr, request); } catch (e) { dispatch.error.call(xhr, e); return; } dispatch.load.call(xhr, result); } else { dispatch.error.call(xhr, request); } } request.onprogress = function(event) { var o = d3.event; d3.event = event; try { dispatch.progress.call(xhr, request); } finally { d3.event = o; } }; xhr.header = function(name, value) { name = (name + "").toLowerCase(); if (arguments.length < 2) return headers[name]; if (value == null) delete headers[name]; else headers[name] = value + ""; return xhr; }; xhr.mimeType = function(value) { if (!arguments.length) return mimeType; mimeType = value == null ? null : value + ""; return xhr; }; xhr.responseType = function(value) { if (!arguments.length) return responseType; responseType = value; return xhr; }; xhr.response = function(value) { response = value; return xhr; }; [ "get", "post" ].forEach(function(method) { xhr[method] = function() { return xhr.send.apply(xhr, [ method ].concat(d3_array(arguments))); }; }); xhr.send = function(method, data, callback) { if (arguments.length === 2 && typeof data === "function") callback = data, data = null; request.open(method, url, true); if (mimeType != null && !("accept" in headers)) headers["accept"] = mimeType + ",*/*"; if (request.setRequestHeader) for (var name in headers) request.setRequestHeader(name, headers[name]); if (mimeType != null && request.overrideMimeType) request.overrideMimeType(mimeType); if (responseType != null) request.responseType = responseType; if (callback != null) xhr.on("error", callback).on("load", function(request) { callback(null, request); }); dispatch.beforesend.call(xhr, request); request.send(data == null ? null : data); return xhr; }; xhr.abort = function() { request.abort(); return xhr; }; d3.rebind(xhr, dispatch, "on"); return callback == null ? xhr : xhr.get(d3_xhr_fixCallback(callback)); } function d3_xhr_fixCallback(callback) { return callback.length === 1 ? function(error, request) { callback(error == null ? request : null); } : callback; } function d3_xhrHasResponse(request) { var type = request.responseType; return type && type !== "text" ? request.response : request.responseText; } d3.dsv = function(delimiter, mimeType) { var reFormat = new RegExp('["' + delimiter + "\n]"), delimiterCode = delimiter.charCodeAt(0); function dsv(url, row, callback) { if (arguments.length < 3) callback = row, row = null; var xhr = d3_xhr(url, mimeType, row == null ? response : typedResponse(row), callback); xhr.row = function(_) { return arguments.length ? xhr.response((row = _) == null ? response : typedResponse(_)) : row; }; return xhr; } function response(request) { return dsv.parse(request.responseText); } function typedResponse(f) { return function(request) { return dsv.parse(request.responseText, f); }; } dsv.parse = function(text, f) { var o; return dsv.parseRows(text, function(row, i) { if (o) return o(row, i - 1); var a = new Function("d", "return {" + row.map(function(name, i) { return JSON.stringify(name) + ": d[" + i + "]"; }).join(",") + "}"); o = f ? function(row, i) { return f(a(row), i); } : a; }); }; dsv.parseRows = function(text, f) { var EOL = {}, EOF = {}, rows = [], N = text.length, I = 0, n = 0, t, eol; function token() { if (I >= N) return EOF; if (eol) return eol = false, EOL; var j = I; if (text.charCodeAt(j) === 34) { var i = j; while (i++ < N) { if (text.charCodeAt(i) === 34) { if (text.charCodeAt(i + 1) !== 34) break; ++i; } } I = i + 2; var c = text.charCodeAt(i + 1); if (c === 13) { eol = true; if (text.charCodeAt(i + 2) === 10) ++I; } else if (c === 10) { eol = true; } return text.slice(j + 1, i).replace(/""/g, '"'); } while (I < N) { var c = text.charCodeAt(I++), k = 1; if (c === 10) eol = true; else if (c === 13) { eol = true; if (text.charCodeAt(I) === 10) ++I, ++k; } else if (c !== delimiterCode) continue; return text.slice(j, I - k); } return text.slice(j); } while ((t = token()) !== EOF) { var a = []; while (t !== EOL && t !== EOF) { a.push(t); t = token(); } if (f && (a = f(a, n++)) == null) continue; rows.push(a); } return rows; }; dsv.format = function(rows) { if (Array.isArray(rows[0])) return dsv.formatRows(rows); var fieldSet = new d3_Set(), fields = []; rows.forEach(function(row) { for (var field in row) { if (!fieldSet.has(field)) { fields.push(fieldSet.add(field)); } } }); return [ fields.map(formatValue).join(delimiter) ].concat(rows.map(function(row) { return fields.map(function(field) { return formatValue(row[field]); }).join(delimiter); })).join("\n"); }; dsv.formatRows = function(rows) { return rows.map(formatRow).join("\n"); }; function formatRow(row) { return row.map(formatValue).join(delimiter); } function formatValue(text) { return reFormat.test(text) ? '"' + text.replace(/\"/g, '""') + '"' : text; } return dsv; }; d3.csv = d3.dsv(",", "text/csv"); d3.tsv = d3.dsv(" ", "text/tab-separated-values"); var d3_timer_queueHead, d3_timer_queueTail, d3_timer_interval, d3_timer_timeout, d3_timer_active, d3_timer_frame = d3_window[d3_vendorSymbol(d3_window, "requestAnimationFrame")] || function(callback) { setTimeout(callback, 17); }; d3.timer = function(callback, delay, then) { var n = arguments.length; if (n < 2) delay = 0; if (n < 3) then = Date.now(); var time = then + delay, timer = { c: callback, t: time, f: false, n: null }; if (d3_timer_queueTail) d3_timer_queueTail.n = timer; else d3_timer_queueHead = timer; d3_timer_queueTail = timer; if (!d3_timer_interval) { d3_timer_timeout = clearTimeout(d3_timer_timeout); d3_timer_interval = 1; d3_timer_frame(d3_timer_step); } }; function d3_timer_step() { var now = d3_timer_mark(), delay = d3_timer_sweep() - now; if (delay > 24) { if (isFinite(delay)) { clearTimeout(d3_timer_timeout); d3_timer_timeout = setTimeout(d3_timer_step, delay); } d3_timer_interval = 0; } else { d3_timer_interval = 1; d3_timer_frame(d3_timer_step); } } d3.timer.flush = function() { d3_timer_mark(); d3_timer_sweep(); }; function d3_timer_mark() { var now = Date.now(); d3_timer_active = d3_timer_queueHead; while (d3_timer_active) { if (now >= d3_timer_active.t) d3_timer_active.f = d3_timer_active.c(now - d3_timer_active.t); d3_timer_active = d3_timer_active.n; } return now; } function d3_timer_sweep() { var t0, t1 = d3_timer_queueHead, time = Infinity; while (t1) { if (t1.f) { t1 = t0 ? t0.n = t1.n : d3_timer_queueHead = t1.n; } else { if (t1.t < time) time = t1.t; t1 = (t0 = t1).n; } } d3_timer_queueTail = t0; return time; } function d3_format_precision(x, p) { return p - (x ? Math.ceil(Math.log(x) / Math.LN10) : 1); } d3.round = function(x, n) { return n ? Math.round(x * (n = Math.pow(10, n))) / n : Math.round(x); }; var d3_formatPrefixes = [ "y", "z", "a", "f", "p", "n", "µ", "m", "", "k", "M", "G", "T", "P", "E", "Z", "Y" ].map(d3_formatPrefix); d3.formatPrefix = function(value, precision) { var i = 0; if (value) { if (value < 0) value *= -1; if (precision) value = d3.round(value, d3_format_precision(value, precision)); i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); i = Math.max(-24, Math.min(24, Math.floor((i - 1) / 3) * 3)); } return d3_formatPrefixes[8 + i / 3]; }; function d3_formatPrefix(d, i) { var k = Math.pow(10, abs(8 - i) * 3); return { scale: i > 8 ? function(d) { return d / k; } : function(d) { return d * k; }, symbol: d }; } function d3_locale_numberFormat(locale) { var locale_decimal = locale.decimal, locale_thousands = locale.thousands, locale_grouping = locale.grouping, locale_currency = locale.currency, formatGroup = locale_grouping && locale_thousands ? function(value, width) { var i = value.length, t = [], j = 0, g = locale_grouping[0], length = 0; while (i > 0 && g > 0) { if (length + g + 1 > width) g = Math.max(1, width - length); t.push(value.substring(i -= g, i + g)); if ((length += g + 1) > width) break; g = locale_grouping[j = (j + 1) % locale_grouping.length]; } return t.reverse().join(locale_thousands); } : d3_identity; return function(specifier) { var match = d3_format_re.exec(specifier), fill = match[1] || " ", align = match[2] || ">", sign = match[3] || "-", symbol = match[4] || "", zfill = match[5], width = +match[6], comma = match[7], precision = match[8], type = match[9], scale = 1, prefix = "", suffix = "", integer = false, exponent = true; if (precision) precision = +precision.substring(1); if (zfill || fill === "0" && align === "=") { zfill = fill = "0"; align = "="; } switch (type) { case "n": comma = true; type = "g"; break; case "%": scale = 100; suffix = "%"; type = "f"; break; case "p": scale = 100; suffix = "%"; type = "r"; break; case "b": case "o": case "x": case "X": if (symbol === "#") prefix = "0" + type.toLowerCase(); case "c": exponent = false; case "d": integer = true; precision = 0; break; case "s": scale = -1; type = "r"; break; } if (symbol === "$") prefix = locale_currency[0], suffix = locale_currency[1]; if (type == "r" && !precision) type = "g"; if (precision != null) { if (type == "g") precision = Math.max(1, Math.min(21, precision)); else if (type == "e" || type == "f") precision = Math.max(0, Math.min(20, precision)); } type = d3_format_types.get(type) || d3_format_typeDefault; var zcomma = zfill && comma; return function(value) { var fullSuffix = suffix; if (integer && value % 1) return ""; var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, "-") : sign === "-" ? "" : sign; if (scale < 0) { var unit = d3.formatPrefix(value, precision); value = unit.scale(value); fullSuffix = unit.symbol + suffix; } else { value *= scale; } value = type(value, precision); var i = value.lastIndexOf("."), before, after; if (i < 0) { var j = exponent ? value.lastIndexOf("e") : -1; if (j < 0) before = value, after = ""; else before = value.substring(0, j), after = value.substring(j); } else { before = value.substring(0, i); after = locale_decimal + value.substring(i + 1); } if (!zfill && comma) before = formatGroup(before, Infinity); var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length), padding = length < width ? new Array(length = width - length + 1).join(fill) : ""; if (zcomma) before = formatGroup(padding + before, padding.length ? width - after.length : Infinity); negative += prefix; value = before + after; return (align === "<" ? negative + value + padding : align === ">" ? padding + negative + value : align === "^" ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) : negative + (zcomma ? value : padding + value)) + fullSuffix; }; }; } var d3_format_re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i; var d3_format_types = d3.map({ b: function(x) { return x.toString(2); }, c: function(x) { return String.fromCharCode(x); }, o: function(x) { return x.toString(8); }, x: function(x) { return x.toString(16); }, X: function(x) { return x.toString(16).toUpperCase(); }, g: function(x, p) { return x.toPrecision(p); }, e: function(x, p) { return x.toExponential(p); }, f: function(x, p) { return x.toFixed(p); }, r: function(x, p) { return (x = d3.round(x, d3_format_precision(x, p))).toFixed(Math.max(0, Math.min(20, d3_format_precision(x * (1 + 1e-15), p)))); } }); function d3_format_typeDefault(x) { return x + ""; } var d3_time = d3.time = {}, d3_date = Date; function d3_date_utc() { this._ = new Date(arguments.length > 1 ? Date.UTC.apply(this, arguments) : arguments[0]); } d3_date_utc.prototype = { getDate: function() { return this._.getUTCDate(); }, getDay: function() { return this._.getUTCDay(); }, getFullYear: function() { return this._.getUTCFullYear(); }, getHours: function() { return this._.getUTCHours(); }, getMilliseconds: function() { return this._.getUTCMilliseconds(); }, getMinutes: function() { return this._.getUTCMinutes(); }, getMonth: function() { return this._.getUTCMonth(); }, getSeconds: function() { return this._.getUTCSeconds(); }, getTime: function() { return this._.getTime(); }, getTimezoneOffset: function() { return 0; }, valueOf: function() { return this._.valueOf(); }, setDate: function() { d3_time_prototype.setUTCDate.apply(this._, arguments); }, setDay: function() { d3_time_prototype.setUTCDay.apply(this._, arguments); }, setFullYear: function() { d3_time_prototype.setUTCFullYear.apply(this._, arguments); }, setHours: function() { d3_time_prototype.setUTCHours.apply(this._, arguments); }, setMilliseconds: function() { d3_time_prototype.setUTCMilliseconds.apply(this._, arguments); }, setMinutes: function() { d3_time_prototype.setUTCMinutes.apply(this._, arguments); }, setMonth: function() { d3_time_prototype.setUTCMonth.apply(this._, arguments); }, setSeconds: function() { d3_time_prototype.setUTCSeconds.apply(this._, arguments); }, setTime: function() { d3_time_prototype.setTime.apply(this._, arguments); } }; var d3_time_prototype = Date.prototype; function d3_time_interval(local, step, number) { function round(date) { var d0 = local(date), d1 = offset(d0, 1); return date - d0 < d1 - date ? d0 : d1; } function ceil(date) { step(date = local(new d3_date(date - 1)), 1); return date; } function offset(date, k) { step(date = new d3_date(+date), k); return date; } function range(t0, t1, dt) { var time = ceil(t0), times = []; if (dt > 1) { while (time < t1) { if (!(number(time) % dt)) times.push(new Date(+time)); step(time, 1); } } else { while (time < t1) times.push(new Date(+time)), step(time, 1); } return times; } function range_utc(t0, t1, dt) { try { d3_date = d3_date_utc; var utc = new d3_date_utc(); utc._ = t0; return range(utc, t1, dt); } finally { d3_date = Date; } } local.floor = local; local.round = round; local.ceil = ceil; local.offset = offset; local.range = range; var utc = local.utc = d3_time_interval_utc(local); utc.floor = utc; utc.round = d3_time_interval_utc(round); utc.ceil = d3_time_interval_utc(ceil); utc.offset = d3_time_interval_utc(offset); utc.range = range_utc; return local; } function d3_time_interval_utc(method) { return function(date, k) { try { d3_date = d3_date_utc; var utc = new d3_date_utc(); utc._ = date; return method(utc, k)._; } finally { d3_date = Date; } }; } d3_time.year = d3_time_interval(function(date) { date = d3_time.day(date); date.setMonth(0, 1); return date; }, function(date, offset) { date.setFullYear(date.getFullYear() + offset); }, function(date) { return date.getFullYear(); }); d3_time.years = d3_time.year.range; d3_time.years.utc = d3_time.year.utc.range; d3_time.day = d3_time_interval(function(date) { var day = new d3_date(2e3, 0); day.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); return day; }, function(date, offset) { date.setDate(date.getDate() + offset); }, function(date) { return date.getDate() - 1; }); d3_time.days = d3_time.day.range; d3_time.days.utc = d3_time.day.utc.range; d3_time.dayOfYear = function(date) { var year = d3_time.year(date); return Math.floor((date - year - (date.getTimezoneOffset() - year.getTimezoneOffset()) * 6e4) / 864e5); }; [ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" ].forEach(function(day, i) { i = 7 - i; var interval = d3_time[day] = d3_time_interval(function(date) { (date = d3_time.day(date)).setDate(date.getDate() - (date.getDay() + i) % 7); return date; }, function(date, offset) { date.setDate(date.getDate() + Math.floor(offset) * 7); }, function(date) { var day = d3_time.year(date).getDay(); return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7) - (day !== i); }); d3_time[day + "s"] = interval.range; d3_time[day + "s"].utc = interval.utc.range; d3_time[day + "OfYear"] = function(date) { var day = d3_time.year(date).getDay(); return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7); }; }); d3_time.week = d3_time.sunday; d3_time.weeks = d3_time.sunday.range; d3_time.weeks.utc = d3_time.sunday.utc.range; d3_time.weekOfYear = d3_time.sundayOfYear; function d3_locale_timeFormat(locale) { var locale_dateTime = locale.dateTime, locale_date = locale.date, locale_time = locale.time, locale_periods = locale.periods, locale_days = locale.days, locale_shortDays = locale.shortDays, locale_months = locale.months, locale_shortMonths = locale.shortMonths; function d3_time_format(template) { var n = template.length; function format(date) { var string = [], i = -1, j = 0, c, p, f; while (++i < n) { if (template.charCodeAt(i) === 37) { string.push(template.slice(j, i)); if ((p = d3_time_formatPads[c = template.charAt(++i)]) != null) c = template.charAt(++i); if (f = d3_time_formats[c]) c = f(date, p == null ? c === "e" ? " " : "0" : p); string.push(c); j = i + 1; } } string.push(template.slice(j, i)); return string.join(""); } format.parse = function(string) { var d = { y: 1900, m: 0, d: 1, H: 0, M: 0, S: 0, L: 0, Z: null }, i = d3_time_parse(d, template, string, 0); if (i != string.length) return null; if ("p" in d) d.H = d.H % 12 + d.p * 12; var localZ = d.Z != null && d3_date !== d3_date_utc, date = new (localZ ? d3_date_utc : d3_date)(); if ("j" in d) date.setFullYear(d.y, 0, d.j); else if ("w" in d && ("W" in d || "U" in d)) { date.setFullYear(d.y, 0, 1); date.setFullYear(d.y, 0, "W" in d ? (d.w + 6) % 7 + d.W * 7 - (date.getDay() + 5) % 7 : d.w + d.U * 7 - (date.getDay() + 6) % 7); } else date.setFullYear(d.y, d.m, d.d); date.setHours(d.H + (d.Z / 100 | 0), d.M + d.Z % 100, d.S, d.L); return localZ ? date._ : date; }; format.toString = function() { return template; }; return format; } function d3_time_parse(date, template, string, j) { var c, p, t, i = 0, n = template.length, m = string.length; while (i < n) { if (j >= m) return -1; c = template.charCodeAt(i++); if (c === 37) { t = template.charAt(i++); p = d3_time_parsers[t in d3_time_formatPads ? template.charAt(i++) : t]; if (!p || (j = p(date, string, j)) < 0) return -1; } else if (c != string.charCodeAt(j++)) { return -1; } } return j; } d3_time_format.utc = function(template) { var local = d3_time_format(template); function format(date) { try { d3_date = d3_date_utc; var utc = new d3_date(); utc._ = date; return local(utc); } finally { d3_date = Date; } } format.parse = function(string) { try { d3_date = d3_date_utc; var date = local.parse(string); return date && date._; } finally { d3_date = Date; } }; format.toString = local.toString; return format; }; d3_time_format.multi = d3_time_format.utc.multi = d3_time_formatMulti; var d3_time_periodLookup = d3.map(), d3_time_dayRe = d3_time_formatRe(locale_days), d3_time_dayLookup = d3_time_formatLookup(locale_days), d3_time_dayAbbrevRe = d3_time_formatRe(locale_shortDays), d3_time_dayAbbrevLookup = d3_time_formatLookup(locale_shortDays), d3_time_monthRe = d3_time_formatRe(locale_months), d3_time_monthLookup = d3_time_formatLookup(locale_months), d3_time_monthAbbrevRe = d3_time_formatRe(locale_shortMonths), d3_time_monthAbbrevLookup = d3_time_formatLookup(locale_shortMonths); locale_periods.forEach(function(p, i) { d3_time_periodLookup.set(p.toLowerCase(), i); }); var d3_time_formats = { a: function(d) { return locale_shortDays[d.getDay()]; }, A: function(d) { return locale_days[d.getDay()]; }, b: function(d) { return locale_shortMonths[d.getMonth()]; }, B: function(d) { return locale_months[d.getMonth()]; }, c: d3_time_format(locale_dateTime), d: function(d, p) { return d3_time_formatPad(d.getDate(), p, 2); }, e: function(d, p) { return d3_time_formatPad(d.getDate(), p, 2); }, H: function(d, p) { return d3_time_formatPad(d.getHours(), p, 2); }, I: function(d, p) { return d3_time_formatPad(d.getHours() % 12 || 12, p, 2); }, j: function(d, p) { return d3_time_formatPad(1 + d3_time.dayOfYear(d), p, 3); }, L: function(d, p) { return d3_time_formatPad(d.getMilliseconds(), p, 3); }, m: function(d, p) { return d3_time_formatPad(d.getMonth() + 1, p, 2); }, M: function(d, p) { return d3_time_formatPad(d.getMinutes(), p, 2); }, p: function(d) { return locale_periods[+(d.getHours() >= 12)]; }, S: function(d, p) { return d3_time_formatPad(d.getSeconds(), p, 2); }, U: function(d, p) { return d3_time_formatPad(d3_time.sundayOfYear(d), p, 2); }, w: function(d) { return d.getDay(); }, W: function(d, p) { return d3_time_formatPad(d3_time.mondayOfYear(d), p, 2); }, x: d3_time_format(locale_date), X: d3_time_format(locale_time), y: function(d, p) { return d3_time_formatPad(d.getFullYear() % 100, p, 2); }, Y: function(d, p) { return d3_time_formatPad(d.getFullYear() % 1e4, p, 4); }, Z: d3_time_zone, "%": function() { return "%"; } }; var d3_time_parsers = { a: d3_time_parseWeekdayAbbrev, A: d3_time_parseWeekday, b: d3_time_parseMonthAbbrev, B: d3_time_parseMonth, c: d3_time_parseLocaleFull, d: d3_time_parseDay, e: d3_time_parseDay, H: d3_time_parseHour24, I: d3_time_parseHour24, j: d3_time_parseDayOfYear, L: d3_time_parseMilliseconds, m: d3_time_parseMonthNumber, M: d3_time_parseMinutes, p: d3_time_parseAmPm, S: d3_time_parseSeconds, U: d3_time_parseWeekNumberSunday, w: d3_time_parseWeekdayNumber, W: d3_time_parseWeekNumberMonday, x: d3_time_parseLocaleDate, X: d3_time_parseLocaleTime, y: d3_time_parseYear, Y: d3_time_parseFullYear, Z: d3_time_parseZone, "%": d3_time_parseLiteralPercent }; function d3_time_parseWeekdayAbbrev(date, string, i) { d3_time_dayAbbrevRe.lastIndex = 0; var n = d3_time_dayAbbrevRe.exec(string.slice(i)); return n ? (date.w = d3_time_dayAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; } function d3_time_parseWeekday(date, string, i) { d3_time_dayRe.lastIndex = 0; var n = d3_time_dayRe.exec(string.slice(i)); return n ? (date.w = d3_time_dayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; } function d3_time_parseMonthAbbrev(date, string, i) { d3_time_monthAbbrevRe.lastIndex = 0; var n = d3_time_monthAbbrevRe.exec(string.slice(i)); return n ? (date.m = d3_time_monthAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; } function d3_time_parseMonth(date, string, i) { d3_time_monthRe.lastIndex = 0; var n = d3_time_monthRe.exec(string.slice(i)); return n ? (date.m = d3_time_monthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; } function d3_time_parseLocaleFull(date, string, i) { return d3_time_parse(date, d3_time_formats.c.toString(), string, i); } function d3_time_parseLocaleDate(date, string, i) { return d3_time_parse(date, d3_time_formats.x.toString(), string, i); } function d3_time_parseLocaleTime(date, string, i) { return d3_time_parse(date, d3_time_formats.X.toString(), string, i); } function d3_time_parseAmPm(date, string, i) { var n = d3_time_periodLookup.get(string.slice(i, i += 2).toLowerCase()); return n == null ? -1 : (date.p = n, i); } return d3_time_format; } var d3_time_formatPads = { "-": "", _: " ", "0": "0" }, d3_time_numberRe = /^\s*\d+/, d3_time_percentRe = /^%/; function d3_time_formatPad(value, fill, width) { var sign = value < 0 ? "-" : "", string = (sign ? -value : value) + "", length = string.length; return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string); } function d3_time_formatRe(names) { return new RegExp("^(?:" + names.map(d3.requote).join("|") + ")", "i"); } function d3_time_formatLookup(names) { var map = new d3_Map(), i = -1, n = names.length; while (++i < n) map.set(names[i].toLowerCase(), i); return map; } function d3_time_parseWeekdayNumber(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i, i + 1)); return n ? (date.w = +n[0], i + n[0].length) : -1; } function d3_time_parseWeekNumberSunday(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i)); return n ? (date.U = +n[0], i + n[0].length) : -1; } function d3_time_parseWeekNumberMonday(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i)); return n ? (date.W = +n[0], i + n[0].length) : -1; } function d3_time_parseFullYear(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i, i + 4)); return n ? (date.y = +n[0], i + n[0].length) : -1; } function d3_time_parseYear(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.y = d3_time_expandYear(+n[0]), i + n[0].length) : -1; } function d3_time_parseZone(date, string, i) { return /^[+-]\d{4}$/.test(string = string.slice(i, i + 5)) ? (date.Z = -string, i + 5) : -1; } function d3_time_expandYear(d) { return d + (d > 68 ? 1900 : 2e3); } function d3_time_parseMonthNumber(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.m = n[0] - 1, i + n[0].length) : -1; } function d3_time_parseDay(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.d = +n[0], i + n[0].length) : -1; } function d3_time_parseDayOfYear(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i, i + 3)); return n ? (date.j = +n[0], i + n[0].length) : -1; } function d3_time_parseHour24(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.H = +n[0], i + n[0].length) : -1; } function d3_time_parseMinutes(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.M = +n[0], i + n[0].length) : -1; } function d3_time_parseSeconds(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.S = +n[0], i + n[0].length) : -1; } function d3_time_parseMilliseconds(date, string, i) { d3_time_numberRe.lastIndex = 0; var n = d3_time_numberRe.exec(string.slice(i, i + 3)); return n ? (date.L = +n[0], i + n[0].length) : -1; } function d3_time_zone(d) { var z = d.getTimezoneOffset(), zs = z > 0 ? "-" : "+", zh = abs(z) / 60 | 0, zm = abs(z) % 60; return zs + d3_time_formatPad(zh, "0", 2) + d3_time_formatPad(zm, "0", 2); } function d3_time_parseLiteralPercent(date, string, i) { d3_time_percentRe.lastIndex = 0; var n = d3_time_percentRe.exec(string.slice(i, i + 1)); return n ? i + n[0].length : -1; } function d3_time_formatMulti(formats) { var n = formats.length, i = -1; while (++i < n) formats[i][0] = this(formats[i][0]); return function(date) { var i = 0, f = formats[i]; while (!f[1](date)) f = formats[++i]; return f[0](date); }; } d3.locale = function(locale) { return { numberFormat: d3_locale_numberFormat(locale), timeFormat: d3_locale_timeFormat(locale) }; }; var d3_locale_enUS = d3.locale({ decimal: ".", thousands: ",", grouping: [ 3 ], currency: [ "$", "" ], dateTime: "%a %b %e %X %Y", date: "%m/%d/%Y", time: "%H:%M:%S", periods: [ "AM", "PM" ], days: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], shortDays: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], shortMonths: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] }); d3.format = d3_locale_enUS.numberFormat; d3.geo = {}; function d3_adder() {} d3_adder.prototype = { s: 0, t: 0, add: function(y) { d3_adderSum(y, this.t, d3_adderTemp); d3_adderSum(d3_adderTemp.s, this.s, this); if (this.s) this.t += d3_adderTemp.t; else this.s = d3_adderTemp.t; }, reset: function() { this.s = this.t = 0; }, valueOf: function() { return this.s; } }; var d3_adderTemp = new d3_adder(); function d3_adderSum(a, b, o) { var x = o.s = a + b, bv = x - a, av = x - bv; o.t = a - av + (b - bv); } d3.geo.stream = function(object, listener) { if (object && d3_geo_streamObjectType.hasOwnProperty(object.type)) { d3_geo_streamObjectType[object.type](object, listener); } else { d3_geo_streamGeometry(object, listener); } }; function d3_geo_streamGeometry(geometry, listener) { if (geometry && d3_geo_streamGeometryType.hasOwnProperty(geometry.type)) { d3_geo_streamGeometryType[geometry.type](geometry, listener); } } var d3_geo_streamObjectType = { Feature: function(feature, listener) { d3_geo_streamGeometry(feature.geometry, listener); }, FeatureCollection: function(object, listener) { var features = object.features, i = -1, n = features.length; while (++i < n) d3_geo_streamGeometry(features[i].geometry, listener); } }; var d3_geo_streamGeometryType = { Sphere: function(object, listener) { listener.sphere(); }, Point: function(object, listener) { object = object.coordinates; listener.point(object[0], object[1], object[2]); }, MultiPoint: function(object, listener) { var coordinates = object.coordinates, i = -1, n = coordinates.length; while (++i < n) object = coordinates[i], listener.point(object[0], object[1], object[2]); }, LineString: function(object, listener) { d3_geo_streamLine(object.coordinates, listener, 0); }, MultiLineString: function(object, listener) { var coordinates = object.coordinates, i = -1, n = coordinates.length; while (++i < n) d3_geo_streamLine(coordinates[i], listener, 0); }, Polygon: function(object, listener) { d3_geo_streamPolygon(object.coordinates, listener); }, MultiPolygon: function(object, listener) { var coordinates = object.coordinates, i = -1, n = coordinates.length; while (++i < n) d3_geo_streamPolygon(coordinates[i], listener); }, GeometryCollection: function(object, listener) { var geometries = object.geometries, i = -1, n = geometries.length; while (++i < n) d3_geo_streamGeometry(geometries[i], listener); } }; function d3_geo_streamLine(coordinates, listener, closed) { var i = -1, n = coordinates.length - closed, coordinate; listener.lineStart(); while (++i < n) coordinate = coordinates[i], listener.point(coordinate[0], coordinate[1], coordinate[2]); listener.lineEnd(); } function d3_geo_streamPolygon(coordinates, listener) { var i = -1, n = coordinates.length; listener.polygonStart(); while (++i < n) d3_geo_streamLine(coordinates[i], listener, 1); listener.polygonEnd(); } d3.geo.area = function(object) { d3_geo_areaSum = 0; d3.geo.stream(object, d3_geo_area); return d3_geo_areaSum; }; var d3_geo_areaSum, d3_geo_areaRingSum = new d3_adder(); var d3_geo_area = { sphere: function() { d3_geo_areaSum += 4 * π; }, point: d3_noop, lineStart: d3_noop, lineEnd: d3_noop, polygonStart: function() { d3_geo_areaRingSum.reset(); d3_geo_area.lineStart = d3_geo_areaRingStart; }, polygonEnd: function() { var area = 2 * d3_geo_areaRingSum; d3_geo_areaSum += area < 0 ? 4 * π + area : area; d3_geo_area.lineStart = d3_geo_area.lineEnd = d3_geo_area.point = d3_noop; } }; function d3_geo_areaRingStart() { var λ00, φ00, λ0, cosφ0, sinφ0; d3_geo_area.point = function(λ, φ) { d3_geo_area.point = nextPoint; λ0 = (λ00 = λ) * d3_radians, cosφ0 = Math.cos(φ = (φ00 = φ) * d3_radians / 2 + π / 4), sinφ0 = Math.sin(φ); }; function nextPoint(λ, φ) { λ *= d3_radians; φ = φ * d3_radians / 2 + π / 4; var dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, cosφ = Math.cos(φ), sinφ = Math.sin(φ), k = sinφ0 * sinφ, u = cosφ0 * cosφ + k * Math.cos(adλ), v = k * sdλ * Math.sin(adλ); d3_geo_areaRingSum.add(Math.atan2(v, u)); λ0 = λ, cosφ0 = cosφ, sinφ0 = sinφ; } d3_geo_area.lineEnd = function() { nextPoint(λ00, φ00); }; } function d3_geo_cartesian(spherical) { var λ = spherical[0], φ = spherical[1], cosφ = Math.cos(φ); return [ cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ) ]; } function d3_geo_cartesianDot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; } function d3_geo_cartesianCross(a, b) { return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; } function d3_geo_cartesianAdd(a, b) { a[0] += b[0]; a[1] += b[1]; a[2] += b[2]; } function d3_geo_cartesianScale(vector, k) { return [ vector[0] * k, vector[1] * k, vector[2] * k ]; } function d3_geo_cartesianNormalize(d) { var l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); d[0] /= l; d[1] /= l; d[2] /= l; } function d3_geo_spherical(cartesian) { return [ Math.atan2(cartesian[1], cartesian[0]), d3_asin(cartesian[2]) ]; } function d3_geo_sphericalEqual(a, b) { return abs(a[0] - b[0]) < ε && abs(a[1] - b[1]) < ε; } d3.geo.bounds = function() { var λ0, φ0, λ1, φ1, λ_, λ__, φ__, p0, dλSum, ranges, range; var bound = { point: point, lineStart: lineStart, lineEnd: lineEnd, polygonStart: function() { bound.point = ringPoint; bound.lineStart = ringStart; bound.lineEnd = ringEnd; dλSum = 0; d3_geo_area.polygonStart(); }, polygonEnd: function() { d3_geo_area.polygonEnd(); bound.point = point; bound.lineStart = lineStart; bound.lineEnd = lineEnd; if (d3_geo_areaRingSum < 0) λ0 = -(λ1 = 180), φ0 = -(φ1 = 90); else if (dλSum > ε) φ1 = 90; else if (dλSum < -ε) φ0 = -90; range[0] = λ0, range[1] = λ1; } }; function point(λ, φ) { ranges.push(range = [ λ0 = λ, λ1 = λ ]); if (φ < φ0) φ0 = φ; if (φ > φ1) φ1 = φ; } function linePoint(λ, φ) { var p = d3_geo_cartesian([ λ * d3_radians, φ * d3_radians ]); if (p0) { var normal = d3_geo_cartesianCross(p0, p), equatorial = [ normal[1], -normal[0], 0 ], inflection = d3_geo_cartesianCross(equatorial, normal); d3_geo_cartesianNormalize(inflection); inflection = d3_geo_spherical(inflection); var dλ = λ - λ_, s = dλ > 0 ? 1 : -1, λi = inflection[0] * d3_degrees * s, antimeridian = abs(dλ) > 180; if (antimeridian ^ (s * λ_ < λi && λi < s * λ)) { var φi = inflection[1] * d3_degrees; if (φi > φ1) φ1 = φi; } else if (λi = (λi + 360) % 360 - 180, antimeridian ^ (s * λ_ < λi && λi < s * λ)) { var φi = -inflection[1] * d3_degrees; if (φi < φ0) φ0 = φi; } else { if (φ < φ0) φ0 = φ; if (φ > φ1) φ1 = φ; } if (antimeridian) { if (λ < λ_) { if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ; } else { if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ; } } else { if (λ1 >= λ0) { if (λ < λ0) λ0 = λ; if (λ > λ1) λ1 = λ; } else { if (λ > λ_) { if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ; } else { if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ; } } } } else { point(λ, φ); } p0 = p, λ_ = λ; } function lineStart() { bound.point = linePoint; } function lineEnd() { range[0] = λ0, range[1] = λ1; bound.point = point; p0 = null; } function ringPoint(λ, φ) { if (p0) { var dλ = λ - λ_; dλSum += abs(dλ) > 180 ? dλ + (dλ > 0 ? 360 : -360) : dλ; } else λ__ = λ, φ__ = φ; d3_geo_area.point(λ, φ); linePoint(λ, φ); } function ringStart() { d3_geo_area.lineStart(); } function ringEnd() { ringPoint(λ__, φ__); d3_geo_area.lineEnd(); if (abs(dλSum) > ε) λ0 = -(λ1 = 180); range[0] = λ0, range[1] = λ1; p0 = null; } function angle(λ0, λ1) { return (λ1 -= λ0) < 0 ? λ1 + 360 : λ1; } function compareRanges(a, b) { return a[0] - b[0]; } function withinRange(x, range) { return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x; } return function(feature) { φ1 = λ1 = -(λ0 = φ0 = Infinity); ranges = []; d3.geo.stream(feature, bound); var n = ranges.length; if (n) { ranges.sort(compareRanges); for (var i = 1, a = ranges[0], b, merged = [ a ]; i < n; ++i) { b = ranges[i]; if (withinRange(b[0], a) || withinRange(b[1], a)) { if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1]; if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0]; } else { merged.push(a = b); } } var best = -Infinity, dλ; for (var n = merged.length - 1, i = 0, a = merged[n], b; i <= n; a = b, ++i) { b = merged[i]; if ((dλ = angle(a[1], b[0])) > best) best = dλ, λ0 = b[0], λ1 = a[1]; } } ranges = range = null; return λ0 === Infinity || φ0 === Infinity ? [ [ NaN, NaN ], [ NaN, NaN ] ] : [ [ λ0, φ0 ], [ λ1, φ1 ] ]; }; }(); d3.geo.centroid = function(object) { d3_geo_centroidW0 = d3_geo_centroidW1 = d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0; d3.geo.stream(object, d3_geo_centroid); var x = d3_geo_centroidX2, y = d3_geo_centroidY2, z = d3_geo_centroidZ2, m = x * x + y * y + z * z; if (m < ε2) { x = d3_geo_centroidX1, y = d3_geo_centroidY1, z = d3_geo_centroidZ1; if (d3_geo_centroidW1 < ε) x = d3_geo_centroidX0, y = d3_geo_centroidY0, z = d3_geo_centroidZ0; m = x * x + y * y + z * z; if (m < ε2) return [ NaN, NaN ]; } return [ Math.atan2(y, x) * d3_degrees, d3_asin(z / Math.sqrt(m)) * d3_degrees ]; }; var d3_geo_centroidW0, d3_geo_centroidW1, d3_geo_centroidX0, d3_geo_centroidY0, d3_geo_centroidZ0, d3_geo_centroidX1, d3_geo_centroidY1, d3_geo_centroidZ1, d3_geo_centroidX2, d3_geo_centroidY2, d3_geo_centroidZ2; var d3_geo_centroid = { sphere: d3_noop, point: d3_geo_centroidPoint, lineStart: d3_geo_centroidLineStart, lineEnd: d3_geo_centroidLineEnd, polygonStart: function() { d3_geo_centroid.lineStart = d3_geo_centroidRingStart; }, polygonEnd: function() { d3_geo_centroid.lineStart = d3_geo_centroidLineStart; } }; function d3_geo_centroidPoint(λ, φ) { λ *= d3_radians; var cosφ = Math.cos(φ *= d3_radians); d3_geo_centroidPointXYZ(cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ)); } function d3_geo_centroidPointXYZ(x, y, z) { ++d3_geo_centroidW0; d3_geo_centroidX0 += (x - d3_geo_centroidX0) / d3_geo_centroidW0; d3_geo_centroidY0 += (y - d3_geo_centroidY0) / d3_geo_centroidW0; d3_geo_centroidZ0 += (z - d3_geo_centroidZ0) / d3_geo_centroidW0; } function d3_geo_centroidLineStart() { var x0, y0, z0; d3_geo_centroid.point = function(λ, φ) { λ *= d3_radians; var cosφ = Math.cos(φ *= d3_radians); x0 = cosφ * Math.cos(λ); y0 = cosφ * Math.sin(λ); z0 = Math.sin(φ); d3_geo_centroid.point = nextPoint; d3_geo_centroidPointXYZ(x0, y0, z0); }; function nextPoint(λ, φ) { λ *= d3_radians; var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), w = Math.atan2(Math.sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z); d3_geo_centroidW1 += w; d3_geo_centroidX1 += w * (x0 + (x0 = x)); d3_geo_centroidY1 += w * (y0 + (y0 = y)); d3_geo_centroidZ1 += w * (z0 + (z0 = z)); d3_geo_centroidPointXYZ(x0, y0, z0); } } function d3_geo_centroidLineEnd() { d3_geo_centroid.point = d3_geo_centroidPoint; } function d3_geo_centroidRingStart() { var λ00, φ00, x0, y0, z0; d3_geo_centroid.point = function(λ, φ) { λ00 = λ, φ00 = φ; d3_geo_centroid.point = nextPoint; λ *= d3_radians; var cosφ = Math.cos(φ *= d3_radians); x0 = cosφ * Math.cos(λ); y0 = cosφ * Math.sin(λ); z0 = Math.sin(φ); d3_geo_centroidPointXYZ(x0, y0, z0); }; d3_geo_centroid.lineEnd = function() { nextPoint(λ00, φ00); d3_geo_centroid.lineEnd = d3_geo_centroidLineEnd; d3_geo_centroid.point = d3_geo_centroidPoint; }; function nextPoint(λ, φ) { λ *= d3_radians; var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), cx = y0 * z - z0 * y, cy = z0 * x - x0 * z, cz = x0 * y - y0 * x, m = Math.sqrt(cx * cx + cy * cy + cz * cz), u = x0 * x + y0 * y + z0 * z, v = m && -d3_acos(u) / m, w = Math.atan2(m, u); d3_geo_centroidX2 += v * cx; d3_geo_centroidY2 += v * cy; d3_geo_centroidZ2 += v * cz; d3_geo_centroidW1 += w; d3_geo_centroidX1 += w * (x0 + (x0 = x)); d3_geo_centroidY1 += w * (y0 + (y0 = y)); d3_geo_centroidZ1 += w * (z0 + (z0 = z)); d3_geo_centroidPointXYZ(x0, y0, z0); } } function d3_geo_compose(a, b) { function compose(x, y) { return x = a(x, y), b(x[0], x[1]); } if (a.invert && b.invert) compose.invert = function(x, y) { return x = b.invert(x, y), x && a.invert(x[0], x[1]); }; return compose; } function d3_true() { return true; } function d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener) { var subject = [], clip = []; segments.forEach(function(segment) { if ((n = segment.length - 1) <= 0) return; var n, p0 = segment[0], p1 = segment[n]; if (d3_geo_sphericalEqual(p0, p1)) { listener.lineStart(); for (var i = 0; i < n; ++i) listener.point((p0 = segment[i])[0], p0[1]); listener.lineEnd(); return; } var a = new d3_geo_clipPolygonIntersection(p0, segment, null, true), b = new d3_geo_clipPolygonIntersection(p0, null, a, false); a.o = b; subject.push(a); clip.push(b); a = new d3_geo_clipPolygonIntersection(p1, segment, null, false); b = new d3_geo_clipPolygonIntersection(p1, null, a, true); a.o = b; subject.push(a); clip.push(b); }); clip.sort(compare); d3_geo_clipPolygonLinkCircular(subject); d3_geo_clipPolygonLinkCircular(clip); if (!subject.length) return; for (var i = 0, entry = clipStartInside, n = clip.length; i < n; ++i) { clip[i].e = entry = !entry; } var start = subject[0], points, point; while (1) { var current = start, isSubject = true; while (current.v) if ((current = current.n) === start) return; points = current.z; listener.lineStart(); do { current.v = current.o.v = true; if (current.e) { if (isSubject) { for (var i = 0, n = points.length; i < n; ++i) listener.point((point = points[i])[0], point[1]); } else { interpolate(current.x, current.n.x, 1, listener); } current = current.n; } else { if (isSubject) { points = current.p.z; for (var i = points.length - 1; i >= 0; --i) listener.point((point = points[i])[0], point[1]); } else { interpolate(current.x, current.p.x, -1, listener); } current = current.p; } current = current.o; points = current.z; isSubject = !isSubject; } while (!current.v); listener.lineEnd(); } } function d3_geo_clipPolygonLinkCircular(array) { if (!(n = array.length)) return; var n, i = 0, a = array[0], b; while (++i < n) { a.n = b = array[i]; b.p = a; a = b; } a.n = b = array[0]; b.p = a; } function d3_geo_clipPolygonIntersection(point, points, other, entry) { this.x = point; this.z = points; this.o = other; this.e = entry; this.v = false; this.n = this.p = null; } function d3_geo_clip(pointVisible, clipLine, interpolate, clipStart) { return function(rotate, listener) { var line = clipLine(listener), rotatedClipStart = rotate.invert(clipStart[0], clipStart[1]); var clip = { point: point, lineStart: lineStart, lineEnd: lineEnd, polygonStart: function() { clip.point = pointRing; clip.lineStart = ringStart; clip.lineEnd = ringEnd; segments = []; polygon = []; }, polygonEnd: function() { clip.point = point; clip.lineStart = lineStart; clip.lineEnd = lineEnd; segments = d3.merge(segments); var clipStartInside = d3_geo_pointInPolygon(rotatedClipStart, polygon); if (segments.length) { if (!polygonStarted) listener.polygonStart(), polygonStarted = true; d3_geo_clipPolygon(segments, d3_geo_clipSort, clipStartInside, interpolate, listener); } else if (clipStartInside) { if (!polygonStarted) listener.polygonStart(), polygonStarted = true; listener.lineStart(); interpolate(null, null, 1, listener); listener.lineEnd(); } if (polygonStarted) listener.polygonEnd(), polygonStarted = false; segments = polygon = null; }, sphere: function() { listener.polygonStart(); listener.lineStart(); interpolate(null, null, 1, listener); listener.lineEnd(); listener.polygonEnd(); } }; function point(λ, φ) { var point = rotate(λ, φ); if (pointVisible(λ = point[0], φ = point[1])) listener.point(λ, φ); } function pointLine(λ, φ) { var point = rotate(λ, φ); line.point(point[0], point[1]); } function lineStart() { clip.point = pointLine; line.lineStart(); } function lineEnd() { clip.point = point; line.lineEnd(); } var segments; var buffer = d3_geo_clipBufferListener(), ringListener = clipLine(buffer), polygonStarted = false, polygon, ring; function pointRing(λ, φ) { ring.push([ λ, φ ]); var point = rotate(λ, φ); ringListener.point(point[0], point[1]); } function ringStart() { ringListener.lineStart(); ring = []; } function ringEnd() { pointRing(ring[0][0], ring[0][1]); ringListener.lineEnd(); var clean = ringListener.clean(), ringSegments = buffer.buffer(), segment, n = ringSegments.length; ring.pop(); polygon.push(ring); ring = null; if (!n) return; if (clean & 1) { segment = ringSegments[0]; var n = segment.length - 1, i = -1, point; if (n > 0) { if (!polygonStarted) listener.polygonStart(), polygonStarted = true; listener.lineStart(); while (++i < n) listener.point((point = segment[i])[0], point[1]); listener.lineEnd(); } return; } if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift())); segments.push(ringSegments.filter(d3_geo_clipSegmentLength1)); } return clip; }; } function d3_geo_clipSegmentLength1(segment) { return segment.length > 1; } function d3_geo_clipBufferListener() { var lines = [], line; return { lineStart: function() { lines.push(line = []); }, point: function(λ, φ) { line.push([ λ, φ ]); }, lineEnd: d3_noop, buffer: function() { var buffer = lines; lines = []; line = null; return buffer; }, rejoin: function() { if (lines.length > 1) lines.push(lines.pop().concat(lines.shift())); } }; } function d3_geo_clipSort(a, b) { return ((a = a.x)[0] < 0 ? a[1] - halfπ - ε : halfπ - a[1]) - ((b = b.x)[0] < 0 ? b[1] - halfπ - ε : halfπ - b[1]); } var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate, [ -π, -π / 2 ]); function d3_geo_clipAntimeridianLine(listener) { var λ0 = NaN, φ0 = NaN, sλ0 = NaN, clean; return { lineStart: function() { listener.lineStart(); clean = 1; }, point: function(λ1, φ1) { var sλ1 = λ1 > 0 ? π : -π, dλ = abs(λ1 - λ0); if (abs(dλ - π) < ε) { listener.point(λ0, φ0 = (φ0 + φ1) / 2 > 0 ? halfπ : -halfπ); listener.point(sλ0, φ0); listener.lineEnd(); listener.lineStart(); listener.point(sλ1, φ0); listener.point(λ1, φ0); clean = 0; } else if (sλ0 !== sλ1 && dλ >= π) { if (abs(λ0 - sλ0) < ε) λ0 -= sλ0 * ε; if (abs(λ1 - sλ1) < ε) λ1 -= sλ1 * ε; φ0 = d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1); listener.point(sλ0, φ0); listener.lineEnd(); listener.lineStart(); listener.point(sλ1, φ0); clean = 0; } listener.point(λ0 = λ1, φ0 = φ1); sλ0 = sλ1; }, lineEnd: function() { listener.lineEnd(); λ0 = φ0 = NaN; }, clean: function() { return 2 - clean; } }; } function d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1) { var cosφ0, cosφ1, sinλ0_λ1 = Math.sin(λ0 - λ1); return abs(sinλ0_λ1) > ε ? Math.atan((Math.sin(φ0) * (cosφ1 = Math.cos(φ1)) * Math.sin(λ1) - Math.sin(φ1) * (cosφ0 = Math.cos(φ0)) * Math.sin(λ0)) / (cosφ0 * cosφ1 * sinλ0_λ1)) : (φ0 + φ1) / 2; } function d3_geo_clipAntimeridianInterpolate(from, to, direction, listener) { var φ; if (from == null) { φ = direction * halfπ; listener.point(-π, φ); listener.point(0, φ); listener.point(π, φ); listener.point(π, 0); listener.point(π, -φ); listener.point(0, -φ); listener.point(-π, -φ); listener.point(-π, 0); listener.point(-π, φ); } else if (abs(from[0] - to[0]) > ε) { var s = from[0] < to[0] ? π : -π; φ = direction * s / 2; listener.point(-s, φ); listener.point(0, φ); listener.point(s, φ); } else { listener.point(to[0], to[1]); } } function d3_geo_pointInPolygon(point, polygon) { var meridian = point[0], parallel = point[1], meridianNormal = [ Math.sin(meridian), -Math.cos(meridian), 0 ], polarAngle = 0, winding = 0; d3_geo_areaRingSum.reset(); for (var i = 0, n = polygon.length; i < n; ++i) { var ring = polygon[i], m = ring.length; if (!m) continue; var point0 = ring[0], λ0 = point0[0], φ0 = point0[1] / 2 + π / 4, sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), j = 1; while (true) { if (j === m) j = 0; point = ring[j]; var λ = point[0], φ = point[1] / 2 + π / 4, sinφ = Math.sin(φ), cosφ = Math.cos(φ), dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, antimeridian = adλ > π, k = sinφ0 * sinφ; d3_geo_areaRingSum.add(Math.atan2(k * sdλ * Math.sin(adλ), cosφ0 * cosφ + k * Math.cos(adλ))); polarAngle += antimeridian ? dλ + sdλ * τ : dλ; if (antimeridian ^ λ0 >= meridian ^ λ >= meridian) { var arc = d3_geo_cartesianCross(d3_geo_cartesian(point0), d3_geo_cartesian(point)); d3_geo_cartesianNormalize(arc); var intersection = d3_geo_cartesianCross(meridianNormal, arc); d3_geo_cartesianNormalize(intersection); var φarc = (antimeridian ^ dλ >= 0 ? -1 : 1) * d3_asin(intersection[2]); if (parallel > φarc || parallel === φarc && (arc[0] || arc[1])) { winding += antimeridian ^ dλ >= 0 ? 1 : -1; } } if (!j++) break; λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ, point0 = point; } } return (polarAngle < -ε || polarAngle < ε && d3_geo_areaRingSum < 0) ^ winding & 1; } function d3_geo_clipCircle(radius) { var cr = Math.cos(radius), smallRadius = cr > 0, notHemisphere = abs(cr) > ε, interpolate = d3_geo_circleInterpolate(radius, 6 * d3_radians); return d3_geo_clip(visible, clipLine, interpolate, smallRadius ? [ 0, -radius ] : [ -π, radius - π ]); function visible(λ, φ) { return Math.cos(λ) * Math.cos(φ) > cr; } function clipLine(listener) { var point0, c0, v0, v00, clean; return { lineStart: function() { v00 = v0 = false; clean = 1; }, point: function(λ, φ) { var point1 = [ λ, φ ], point2, v = visible(λ, φ), c = smallRadius ? v ? 0 : code(λ, φ) : v ? code(λ + (λ < 0 ? π : -π), φ) : 0; if (!point0 && (v00 = v0 = v)) listener.lineStart(); if (v !== v0) { point2 = intersect(point0, point1); if (d3_geo_sphericalEqual(point0, point2) || d3_geo_sphericalEqual(point1, point2)) { point1[0] += ε; point1[1] += ε; v = visible(point1[0], point1[1]); } } if (v !== v0) { clean = 0; if (v) { listener.lineStart(); point2 = intersect(point1, point0); listener.point(point2[0], point2[1]); } else { point2 = intersect(point0, point1); listener.point(point2[0], point2[1]); listener.lineEnd(); } point0 = point2; } else if (notHemisphere && point0 && smallRadius ^ v) { var t; if (!(c & c0) && (t = intersect(point1, point0, true))) { clean = 0; if (smallRadius) { listener.lineStart(); listener.point(t[0][0], t[0][1]); listener.point(t[1][0], t[1][1]); listener.lineEnd(); } else { listener.point(t[1][0], t[1][1]); listener.lineEnd(); listener.lineStart(); listener.point(t[0][0], t[0][1]); } } } if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) { listener.point(point1[0], point1[1]); } point0 = point1, v0 = v, c0 = c; }, lineEnd: function() { if (v0) listener.lineEnd(); point0 = null; }, clean: function() { return clean | (v00 && v0) << 1; } }; } function intersect(a, b, two) { var pa = d3_geo_cartesian(a), pb = d3_geo_cartesian(b); var n1 = [ 1, 0, 0 ], n2 = d3_geo_cartesianCross(pa, pb), n2n2 = d3_geo_cartesianDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2; if (!determinant) return !two && a; var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_cartesianCross(n1, n2), A = d3_geo_cartesianScale(n1, c1), B = d3_geo_cartesianScale(n2, c2); d3_geo_cartesianAdd(A, B); var u = n1xn2, w = d3_geo_cartesianDot(A, u), uu = d3_geo_cartesianDot(u, u), t2 = w * w - uu * (d3_geo_cartesianDot(A, A) - 1); if (t2 < 0) return; var t = Math.sqrt(t2), q = d3_geo_cartesianScale(u, (-w - t) / uu); d3_geo_cartesianAdd(q, A); q = d3_geo_spherical(q); if (!two) return q; var λ0 = a[0], λ1 = b[0], φ0 = a[1], φ1 = b[1], z; if (λ1 < λ0) z = λ0, λ0 = λ1, λ1 = z; var δλ = λ1 - λ0, polar = abs(δλ - π) < ε, meridian = polar || δλ < ε; if (!polar && φ1 < φ0) z = φ0, φ0 = φ1, φ1 = z; if (meridian ? polar ? φ0 + φ1 > 0 ^ q[1] < (abs(q[0] - λ0) < ε ? φ0 : φ1) : φ0 <= q[1] && q[1] <= φ1 : δλ > π ^ (λ0 <= q[0] && q[0] <= λ1)) { var q1 = d3_geo_cartesianScale(u, (-w + t) / uu); d3_geo_cartesianAdd(q1, A); return [ q, d3_geo_spherical(q1) ]; } } function code(λ, φ) { var r = smallRadius ? radius : π - radius, code = 0; if (λ < -r) code |= 1; else if (λ > r) code |= 2; if (φ < -r) code |= 4; else if (φ > r) code |= 8; return code; } } function d3_geom_clipLine(x0, y0, x1, y1) { return function(line) { var a = line.a, b = line.b, ax = a.x, ay = a.y, bx = b.x, by = b.y, t0 = 0, t1 = 1, dx = bx - ax, dy = by - ay, r; r = x0 - ax; if (!dx && r > 0) return; r /= dx; if (dx < 0) { if (r < t0) return; if (r < t1) t1 = r; } else if (dx > 0) { if (r > t1) return; if (r > t0) t0 = r; } r = x1 - ax; if (!dx && r < 0) return; r /= dx; if (dx < 0) { if (r > t1) return; if (r > t0) t0 = r; } else if (dx > 0) { if (r < t0) return; if (r < t1) t1 = r; } r = y0 - ay; if (!dy && r > 0) return; r /= dy; if (dy < 0) { if (r < t0) return; if (r < t1) t1 = r; } else if (dy > 0) { if (r > t1) return; if (r > t0) t0 = r; } r = y1 - ay; if (!dy && r < 0) return; r /= dy; if (dy < 0) { if (r > t1) return; if (r > t0) t0 = r; } else if (dy > 0) { if (r < t0) return; if (r < t1) t1 = r; } if (t0 > 0) line.a = { x: ax + t0 * dx, y: ay + t0 * dy }; if (t1 < 1) line.b = { x: ax + t1 * dx, y: ay + t1 * dy }; return line; }; } var d3_geo_clipExtentMAX = 1e9; d3.geo.clipExtent = function() { var x0, y0, x1, y1, stream, clip, clipExtent = { stream: function(output) { if (stream) stream.valid = false; stream = clip(output); stream.valid = true; return stream; }, extent: function(_) { if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ]; clip = d3_geo_clipExtent(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]); if (stream) stream.valid = false, stream = null; return clipExtent; } }; return clipExtent.extent([ [ 0, 0 ], [ 960, 500 ] ]); }; function d3_geo_clipExtent(x0, y0, x1, y1) { return function(listener) { var listener_ = listener, bufferListener = d3_geo_clipBufferListener(), clipLine = d3_geom_clipLine(x0, y0, x1, y1), segments, polygon, ring; var clip = { point: point, lineStart: lineStart, lineEnd: lineEnd, polygonStart: function() { listener = bufferListener; segments = []; polygon = []; clean = true; }, polygonEnd: function() { listener = listener_; segments = d3.merge(segments); var clipStartInside = insidePolygon([ x0, y1 ]), inside = clean && clipStartInside, visible = segments.length; if (inside || visible) { listener.polygonStart(); if (inside) { listener.lineStart(); interpolate(null, null, 1, listener); listener.lineEnd(); } if (visible) { d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener); } listener.polygonEnd(); } segments = polygon = ring = null; } }; function insidePolygon(p) { var wn = 0, n = polygon.length, y = p[1]; for (var i = 0; i < n; ++i) { for (var j = 1, v = polygon[i], m = v.length, a = v[0], b; j < m; ++j) { b = v[j]; if (a[1] <= y) { if (b[1] > y && d3_cross2d(a, b, p) > 0) ++wn; } else { if (b[1] <= y && d3_cross2d(a, b, p) < 0) --wn; } a = b; } } return wn !== 0; } function interpolate(from, to, direction, listener) { var a = 0, a1 = 0; if (from == null || (a = corner(from, direction)) !== (a1 = corner(to, direction)) || comparePoints(from, to) < 0 ^ direction > 0) { do { listener.point(a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0); } while ((a = (a + direction + 4) % 4) !== a1); } else { listener.point(to[0], to[1]); } } function pointVisible(x, y) { return x0 <= x && x <= x1 && y0 <= y && y <= y1; } function point(x, y) { if (pointVisible(x, y)) listener.point(x, y); } var x__, y__, v__, x_, y_, v_, first, clean; function lineStart() { clip.point = linePoint; if (polygon) polygon.push(ring = []); first = true; v_ = false; x_ = y_ = NaN; } function lineEnd() { if (segments) { linePoint(x__, y__); if (v__ && v_) bufferListener.rejoin(); segments.push(bufferListener.buffer()); } clip.point = point; if (v_) listener.lineEnd(); } function linePoint(x, y) { x = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, x)); y = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, y)); var v = pointVisible(x, y); if (polygon) ring.push([ x, y ]); if (first) { x__ = x, y__ = y, v__ = v; first = false; if (v) { listener.lineStart(); listener.point(x, y); } } else { if (v && v_) listener.point(x, y); else { var l = { a: { x: x_, y: y_ }, b: { x: x, y: y } }; if (clipLine(l)) { if (!v_) { listener.lineStart(); listener.point(l.a.x, l.a.y); } listener.point(l.b.x, l.b.y); if (!v) listener.lineEnd(); clean = false; } else if (v) { listener.lineStart(); listener.point(x, y); clean = false; } } } x_ = x, y_ = y, v_ = v; } return clip; }; function corner(p, direction) { return abs(p[0] - x0) < ε ? direction > 0 ? 0 : 3 : abs(p[0] - x1) < ε ? direction > 0 ? 2 : 1 : abs(p[1] - y0) < ε ? direction > 0 ? 1 : 0 : direction > 0 ? 3 : 2; } function compare(a, b) { return comparePoints(a.x, b.x); } function comparePoints(a, b) { var ca = corner(a, 1), cb = corner(b, 1); return ca !== cb ? ca - cb : ca === 0 ? b[1] - a[1] : ca === 1 ? a[0] - b[0] : ca === 2 ? a[1] - b[1] : b[0] - a[0]; } } function d3_geo_conic(projectAt) { var φ0 = 0, φ1 = π / 3, m = d3_geo_projectionMutator(projectAt), p = m(φ0, φ1); p.parallels = function(_) { if (!arguments.length) return [ φ0 / π * 180, φ1 / π * 180 ]; return m(φ0 = _[0] * π / 180, φ1 = _[1] * π / 180); }; return p; } function d3_geo_conicEqualArea(φ0, φ1) { var sinφ0 = Math.sin(φ0), n = (sinφ0 + Math.sin(φ1)) / 2, C = 1 + sinφ0 * (2 * n - sinφ0), ρ0 = Math.sqrt(C) / n; function forward(λ, φ) { var ρ = Math.sqrt(C - 2 * n * Math.sin(φ)) / n; return [ ρ * Math.sin(λ *= n), ρ0 - ρ * Math.cos(λ) ]; } forward.invert = function(x, y) { var ρ0_y = ρ0 - y; return [ Math.atan2(x, ρ0_y) / n, d3_asin((C - (x * x + ρ0_y * ρ0_y) * n * n) / (2 * n)) ]; }; return forward; } (d3.geo.conicEqualArea = function() { return d3_geo_conic(d3_geo_conicEqualArea); }).raw = d3_geo_conicEqualArea; d3.geo.albers = function() { return d3.geo.conicEqualArea().rotate([ 96, 0 ]).center([ -.6, 38.7 ]).parallels([ 29.5, 45.5 ]).scale(1070); }; d3.geo.albersUsa = function() { var lower48 = d3.geo.albers(); var alaska = d3.geo.conicEqualArea().rotate([ 154, 0 ]).center([ -2, 58.5 ]).parallels([ 55, 65 ]); var hawaii = d3.geo.conicEqualArea().rotate([ 157, 0 ]).center([ -3, 19.9 ]).parallels([ 8, 18 ]); var point, pointStream = { point: function(x, y) { point = [ x, y ]; } }, lower48Point, alaskaPoint, hawaiiPoint; function albersUsa(coordinates) { var x = coordinates[0], y = coordinates[1]; point = null; (lower48Point(x, y), point) || (alaskaPoint(x, y), point) || hawaiiPoint(x, y); return point; } albersUsa.invert = function(coordinates) { var k = lower48.scale(), t = lower48.translate(), x = (coordinates[0] - t[0]) / k, y = (coordinates[1] - t[1]) / k; return (y >= .12 && y < .234 && x >= -.425 && x < -.214 ? alaska : y >= .166 && y < .234 && x >= -.214 && x < -.115 ? hawaii : lower48).invert(coordinates); }; albersUsa.stream = function(stream) { var lower48Stream = lower48.stream(stream), alaskaStream = alaska.stream(stream), hawaiiStream = hawaii.stream(stream); return { point: function(x, y) { lower48Stream.point(x, y); alaskaStream.point(x, y); hawaiiStream.point(x, y); }, sphere: function() { lower48Stream.sphere(); alaskaStream.sphere(); hawaiiStream.sphere(); }, lineStart: function() { lower48Stream.lineStart(); alaskaStream.lineStart(); hawaiiStream.lineStart(); }, lineEnd: function() { lower48Stream.lineEnd(); alaskaStream.lineEnd(); hawaiiStream.lineEnd(); }, polygonStart: function() { lower48Stream.polygonStart(); alaskaStream.polygonStart(); hawaiiStream.polygonStart(); }, polygonEnd: function() { lower48Stream.polygonEnd(); alaskaStream.polygonEnd(); hawaiiStream.polygonEnd(); } }; }; albersUsa.precision = function(_) { if (!arguments.length) return lower48.precision(); lower48.precision(_); alaska.precision(_); hawaii.precision(_); return albersUsa; }; albersUsa.scale = function(_) { if (!arguments.length) return lower48.scale(); lower48.scale(_); alaska.scale(_ * .35); hawaii.scale(_); return albersUsa.translate(lower48.translate()); }; albersUsa.translate = function(_) { if (!arguments.length) return lower48.translate(); var k = lower48.scale(), x = +_[0], y = +_[1]; lower48Point = lower48.translate(_).clipExtent([ [ x - .455 * k, y - .238 * k ], [ x + .455 * k, y + .238 * k ] ]).stream(pointStream).point; alaskaPoint = alaska.translate([ x - .307 * k, y + .201 * k ]).clipExtent([ [ x - .425 * k + ε, y + .12 * k + ε ], [ x - .214 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point; hawaiiPoint = hawaii.translate([ x - .205 * k, y + .212 * k ]).clipExtent([ [ x - .214 * k + ε, y + .166 * k + ε ], [ x - .115 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point; return albersUsa; }; return albersUsa.scale(1070); }; var d3_geo_pathAreaSum, d3_geo_pathAreaPolygon, d3_geo_pathArea = { point: d3_noop, lineStart: d3_noop, lineEnd: d3_noop, polygonStart: function() { d3_geo_pathAreaPolygon = 0; d3_geo_pathArea.lineStart = d3_geo_pathAreaRingStart; }, polygonEnd: function() { d3_geo_pathArea.lineStart = d3_geo_pathArea.lineEnd = d3_geo_pathArea.point = d3_noop; d3_geo_pathAreaSum += abs(d3_geo_pathAreaPolygon / 2); } }; function d3_geo_pathAreaRingStart() { var x00, y00, x0, y0; d3_geo_pathArea.point = function(x, y) { d3_geo_pathArea.point = nextPoint; x00 = x0 = x, y00 = y0 = y; }; function nextPoint(x, y) { d3_geo_pathAreaPolygon += y0 * x - x0 * y; x0 = x, y0 = y; } d3_geo_pathArea.lineEnd = function() { nextPoint(x00, y00); }; } var d3_geo_pathBoundsX0, d3_geo_pathBoundsY0, d3_geo_pathBoundsX1, d3_geo_pathBoundsY1; var d3_geo_pathBounds = { point: d3_geo_pathBoundsPoint, lineStart: d3_noop, lineEnd: d3_noop, polygonStart: d3_noop, polygonEnd: d3_noop }; function d3_geo_pathBoundsPoint(x, y) { if (x < d3_geo_pathBoundsX0) d3_geo_pathBoundsX0 = x; if (x > d3_geo_pathBoundsX1) d3_geo_pathBoundsX1 = x; if (y < d3_geo_pathBoundsY0) d3_geo_pathBoundsY0 = y; if (y > d3_geo_pathBoundsY1) d3_geo_pathBoundsY1 = y; } function d3_geo_pathBuffer() { var pointCircle = d3_geo_pathBufferCircle(4.5), buffer = []; var stream = { point: point, lineStart: function() { stream.point = pointLineStart; }, lineEnd: lineEnd, polygonStart: function() { stream.lineEnd = lineEndPolygon; }, polygonEnd: function() { stream.lineEnd = lineEnd; stream.point = point; }, pointRadius: function(_) { pointCircle = d3_geo_pathBufferCircle(_); return stream; }, result: function() { if (buffer.length) { var result = buffer.join(""); buffer = []; return result; } } }; function point(x, y) { buffer.push("M", x, ",", y, pointCircle); } function pointLineStart(x, y) { buffer.push("M", x, ",", y); stream.point = pointLine; } function pointLine(x, y) { buffer.push("L", x, ",", y); } function lineEnd() { stream.point = point; } function lineEndPolygon() { buffer.push("Z"); } return stream; } function d3_geo_pathBufferCircle(radius) { return "m0," + radius + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius + "a" + radius + "," + radius + " 0 1,1 0," + 2 * radius + "z"; } var d3_geo_pathCentroid = { point: d3_geo_pathCentroidPoint, lineStart: d3_geo_pathCentroidLineStart, lineEnd: d3_geo_pathCentroidLineEnd, polygonStart: function() { d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidRingStart; }, polygonEnd: function() { d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidLineStart; d3_geo_pathCentroid.lineEnd = d3_geo_pathCentroidLineEnd; } }; function d3_geo_pathCentroidPoint(x, y) { d3_geo_centroidX0 += x; d3_geo_centroidY0 += y; ++d3_geo_centroidZ0; } function d3_geo_pathCentroidLineStart() { var x0, y0; d3_geo_pathCentroid.point = function(x, y) { d3_geo_pathCentroid.point = nextPoint; d3_geo_pathCentroidPoint(x0 = x, y0 = y); }; function nextPoint(x, y) { var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy); d3_geo_centroidX1 += z * (x0 + x) / 2; d3_geo_centroidY1 += z * (y0 + y) / 2; d3_geo_centroidZ1 += z; d3_geo_pathCentroidPoint(x0 = x, y0 = y); } } function d3_geo_pathCentroidLineEnd() { d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; } function d3_geo_pathCentroidRingStart() { var x00, y00, x0, y0; d3_geo_pathCentroid.point = function(x, y) { d3_geo_pathCentroid.point = nextPoint; d3_geo_pathCentroidPoint(x00 = x0 = x, y00 = y0 = y); }; function nextPoint(x, y) { var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy); d3_geo_centroidX1 += z * (x0 + x) / 2; d3_geo_centroidY1 += z * (y0 + y) / 2; d3_geo_centroidZ1 += z; z = y0 * x - x0 * y; d3_geo_centroidX2 += z * (x0 + x); d3_geo_centroidY2 += z * (y0 + y); d3_geo_centroidZ2 += z * 3; d3_geo_pathCentroidPoint(x0 = x, y0 = y); } d3_geo_pathCentroid.lineEnd = function() { nextPoint(x00, y00); }; } function d3_geo_pathContext(context) { var pointRadius = 4.5; var stream = { point: point, lineStart: function() { stream.point = pointLineStart; }, lineEnd: lineEnd, polygonStart: function() { stream.lineEnd = lineEndPolygon; }, polygonEnd: function() { stream.lineEnd = lineEnd; stream.point = point; }, pointRadius: function(_) { pointRadius = _; return stream; }, result: d3_noop }; function point(x, y) { context.moveTo(x + pointRadius, y); context.arc(x, y, pointRadius, 0, τ); } function pointLineStart(x, y) { context.moveTo(x, y); stream.point = pointLine; } function pointLine(x, y) { context.lineTo(x, y); } function lineEnd() { stream.point = point; } function lineEndPolygon() { context.closePath(); } return stream; } function d3_geo_resample(project) { var δ2 = .5, cosMinDistance = Math.cos(30 * d3_radians), maxDepth = 16; function resample(stream) { return (maxDepth ? resampleRecursive : resampleNone)(stream); } function resampleNone(stream) { return d3_geo_transformPoint(stream, function(x, y) { x = project(x, y); stream.point(x[0], x[1]); }); } function resampleRecursive(stream) { var λ00, φ00, x00, y00, a00, b00, c00, λ0, x0, y0, a0, b0, c0; var resample = { point: point, lineStart: lineStart, lineEnd: lineEnd, polygonStart: function() { stream.polygonStart(); resample.lineStart = ringStart; }, polygonEnd: function() { stream.polygonEnd(); resample.lineStart = lineStart; } }; function point(x, y) { x = project(x, y); stream.point(x[0], x[1]); } function lineStart() { x0 = NaN; resample.point = linePoint; stream.lineStart(); } function linePoint(λ, φ) { var c = d3_geo_cartesian([ λ, φ ]), p = project(λ, φ); resampleLineTo(x0, y0, λ0, a0, b0, c0, x0 = p[0], y0 = p[1], λ0 = λ, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream); stream.point(x0, y0); } function lineEnd() { resample.point = point; stream.lineEnd(); } function ringStart() { lineStart(); resample.point = ringPoint; resample.lineEnd = ringEnd; } function ringPoint(λ, φ) { linePoint(λ00 = λ, φ00 = φ), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0; resample.point = linePoint; } function ringEnd() { resampleLineTo(x0, y0, λ0, a0, b0, c0, x00, y00, λ00, a00, b00, c00, maxDepth, stream); resample.lineEnd = lineEnd; lineEnd(); } return resample; } function resampleLineTo(x0, y0, λ0, a0, b0, c0, x1, y1, λ1, a1, b1, c1, depth, stream) { var dx = x1 - x0, dy = y1 - y0, d2 = dx * dx + dy * dy; if (d2 > 4 * δ2 && depth--) { var a = a0 + a1, b = b0 + b1, c = c0 + c1, m = Math.sqrt(a * a + b * b + c * c), φ2 = Math.asin(c /= m), λ2 = abs(abs(c) - 1) < ε || abs(λ0 - λ1) < ε ? (λ0 + λ1) / 2 : Math.atan2(b, a), p = project(λ2, φ2), x2 = p[0], y2 = p[1], dx2 = x2 - x0, dy2 = y2 - y0, dz = dy * dx2 - dx * dy2; if (dz * dz / d2 > δ2 || abs((dx * dx2 + dy * dy2) / d2 - .5) > .3 || a0 * a1 + b0 * b1 + c0 * c1 < cosMinDistance) { resampleLineTo(x0, y0, λ0, a0, b0, c0, x2, y2, λ2, a /= m, b /= m, c, depth, stream); stream.point(x2, y2); resampleLineTo(x2, y2, λ2, a, b, c, x1, y1, λ1, a1, b1, c1, depth, stream); } } } resample.precision = function(_) { if (!arguments.length) return Math.sqrt(δ2); maxDepth = (δ2 = _ * _) > 0 && 16; return resample; }; return resample; } d3.geo.path = function() { var pointRadius = 4.5, projection, context, projectStream, contextStream, cacheStream; function path(object) { if (object) { if (typeof pointRadius === "function") contextStream.pointRadius(+pointRadius.apply(this, arguments)); if (!cacheStream || !cacheStream.valid) cacheStream = projectStream(contextStream); d3.geo.stream(object, cacheStream); } return contextStream.result(); } path.area = function(object) { d3_geo_pathAreaSum = 0; d3.geo.stream(object, projectStream(d3_geo_pathArea)); return d3_geo_pathAreaSum; }; path.centroid = function(object) { d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0; d3.geo.stream(object, projectStream(d3_geo_pathCentroid)); return d3_geo_centroidZ2 ? [ d3_geo_centroidX2 / d3_geo_centroidZ2, d3_geo_centroidY2 / d3_geo_centroidZ2 ] : d3_geo_centroidZ1 ? [ d3_geo_centroidX1 / d3_geo_centroidZ1, d3_geo_centroidY1 / d3_geo_centroidZ1 ] : d3_geo_centroidZ0 ? [ d3_geo_centroidX0 / d3_geo_centroidZ0, d3_geo_centroidY0 / d3_geo_centroidZ0 ] : [ NaN, NaN ]; }; path.bounds = function(object) { d3_geo_pathBoundsX1 = d3_geo_pathBoundsY1 = -(d3_geo_pathBoundsX0 = d3_geo_pathBoundsY0 = Infinity); d3.geo.stream(object, projectStream(d3_geo_pathBounds)); return [ [ d3_geo_pathBoundsX0, d3_geo_pathBoundsY0 ], [ d3_geo_pathBoundsX1, d3_geo_pathBoundsY1 ] ]; }; path.projection = function(_) { if (!arguments.length) return projection; projectStream = (projection = _) ? _.stream || d3_geo_pathProjectStream(_) : d3_identity; return reset(); }; path.context = function(_) { if (!arguments.length) return context; contextStream = (context = _) == null ? new d3_geo_pathBuffer() : new d3_geo_pathContext(_); if (typeof pointRadius !== "function") contextStream.pointRadius(pointRadius); return reset(); }; path.pointRadius = function(_) { if (!arguments.length) return pointRadius; pointRadius = typeof _ === "function" ? _ : (contextStream.pointRadius(+_), +_); return path; }; function reset() { cacheStream = null; return path; } return path.projection(d3.geo.albersUsa()).context(null); }; function d3_geo_pathProjectStream(project) { var resample = d3_geo_resample(function(x, y) { return project([ x * d3_degrees, y * d3_degrees ]); }); return function(stream) { return d3_geo_projectionRadians(resample(stream)); }; } d3.geo.transform = function(methods) { return { stream: function(stream) { var transform = new d3_geo_transform(stream); for (var k in methods) transform[k] = methods[k]; return transform; } }; }; function d3_geo_transform(stream) { this.stream = stream; } d3_geo_transform.prototype = { point: function(x, y) { this.stream.point(x, y); }, sphere: function() { this.stream.sphere(); }, lineStart: function() { this.stream.lineStart(); }, lineEnd: function() { this.stream.lineEnd(); }, polygonStart: function() { this.stream.polygonStart(); }, polygonEnd: function() { this.stream.polygonEnd(); } }; function d3_geo_transformPoint(stream, point) { return { point: point, sphere: function() { stream.sphere(); }, lineStart: function() { stream.lineStart(); }, lineEnd: function() { stream.lineEnd(); }, polygonStart: function() { stream.polygonStart(); }, polygonEnd: function() { stream.polygonEnd(); } }; } d3.geo.projection = d3_geo_projection; d3.geo.projectionMutator = d3_geo_projectionMutator; function d3_geo_projection(project) { return d3_geo_projectionMutator(function() { return project; })(); } function d3_geo_projectionMutator(projectAt) { var project, rotate, projectRotate, projectResample = d3_geo_resample(function(x, y) { x = project(x, y); return [ x[0] * k + δx, δy - x[1] * k ]; }), k = 150, x = 480, y = 250, λ = 0, φ = 0, δλ = 0, δφ = 0, δγ = 0, δx, δy, preclip = d3_geo_clipAntimeridian, postclip = d3_identity, clipAngle = null, clipExtent = null, stream; function projection(point) { point = projectRotate(point[0] * d3_radians, point[1] * d3_radians); return [ point[0] * k + δx, δy - point[1] * k ]; } function invert(point) { point = projectRotate.invert((point[0] - δx) / k, (δy - point[1]) / k); return point && [ point[0] * d3_degrees, point[1] * d3_degrees ]; } projection.stream = function(output) { if (stream) stream.valid = false; stream = d3_geo_projectionRadians(preclip(rotate, projectResample(postclip(output)))); stream.valid = true; return stream; }; projection.clipAngle = function(_) { if (!arguments.length) return clipAngle; preclip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle((clipAngle = +_) * d3_radians); return invalidate(); }; projection.clipExtent = function(_) { if (!arguments.length) return clipExtent; clipExtent = _; postclip = _ ? d3_geo_clipExtent(_[0][0], _[0][1], _[1][0], _[1][1]) : d3_identity; return invalidate(); }; projection.scale = function(_) { if (!arguments.length) return k; k = +_; return reset(); }; projection.translate = function(_) { if (!arguments.length) return [ x, y ]; x = +_[0]; y = +_[1]; return reset(); }; projection.center = function(_) { if (!arguments.length) return [ λ * d3_degrees, φ * d3_degrees ]; λ = _[0] % 360 * d3_radians; φ = _[1] % 360 * d3_radians; return reset(); }; projection.rotate = function(_) { if (!arguments.length) return [ δλ * d3_degrees, δφ * d3_degrees, δγ * d3_degrees ]; δλ = _[0] % 360 * d3_radians; δφ = _[1] % 360 * d3_radians; δγ = _.length > 2 ? _[2] % 360 * d3_radians : 0; return reset(); }; d3.rebind(projection, projectResample, "precision"); function reset() { projectRotate = d3_geo_compose(rotate = d3_geo_rotation(δλ, δφ, δγ), project); var center = project(λ, φ); δx = x - center[0] * k; δy = y + center[1] * k; return invalidate(); } function invalidate() { if (stream) stream.valid = false, stream = null; return projection; } return function() { project = projectAt.apply(this, arguments); projection.invert = project.invert && invert; return reset(); }; } function d3_geo_projectionRadians(stream) { return d3_geo_transformPoint(stream, function(x, y) { stream.point(x * d3_radians, y * d3_radians); }); } function d3_geo_equirectangular(λ, φ) { return [ λ, φ ]; } (d3.geo.equirectangular = function() { return d3_geo_projection(d3_geo_equirectangular); }).raw = d3_geo_equirectangular.invert = d3_geo_equirectangular; d3.geo.rotation = function(rotate) { rotate = d3_geo_rotation(rotate[0] % 360 * d3_radians, rotate[1] * d3_radians, rotate.length > 2 ? rotate[2] * d3_radians : 0); function forward(coordinates) { coordinates = rotate(coordinates[0] * d3_radians, coordinates[1] * d3_radians); return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates; } forward.invert = function(coordinates) { coordinates = rotate.invert(coordinates[0] * d3_radians, coordinates[1] * d3_radians); return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates; }; return forward; }; function d3_geo_identityRotation(λ, φ) { return [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ]; } d3_geo_identityRotation.invert = d3_geo_equirectangular; function d3_geo_rotation(δλ, δφ, δγ) { return δλ ? δφ || δγ ? d3_geo_compose(d3_geo_rotationλ(δλ), d3_geo_rotationφγ(δφ, δγ)) : d3_geo_rotationλ(δλ) : δφ || δγ ? d3_geo_rotationφγ(δφ, δγ) : d3_geo_identityRotation; } function d3_geo_forwardRotationλ(δλ) { return function(λ, φ) { return λ += δλ, [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ]; }; } function d3_geo_rotationλ(δλ) { var rotation = d3_geo_forwardRotationλ(δλ); rotation.invert = d3_geo_forwardRotationλ(-δλ); return rotation; } function d3_geo_rotationφγ(δφ, δγ) { var cosδφ = Math.cos(δφ), sinδφ = Math.sin(δφ), cosδγ = Math.cos(δγ), sinδγ = Math.sin(δγ); function rotation(λ, φ) { var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδφ + x * sinδφ; return [ Math.atan2(y * cosδγ - k * sinδγ, x * cosδφ - z * sinδφ), d3_asin(k * cosδγ + y * sinδγ) ]; } rotation.invert = function(λ, φ) { var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδγ - y * sinδγ; return [ Math.atan2(y * cosδγ + z * sinδγ, x * cosδφ + k * sinδφ), d3_asin(k * cosδφ - x * sinδφ) ]; }; return rotation; } d3.geo.circle = function() { var origin = [ 0, 0 ], angle, precision = 6, interpolate; function circle() { var center = typeof origin === "function" ? origin.apply(this, arguments) : origin, rotate = d3_geo_rotation(-center[0] * d3_radians, -center[1] * d3_radians, 0).invert, ring = []; interpolate(null, null, 1, { point: function(x, y) { ring.push(x = rotate(x, y)); x[0] *= d3_degrees, x[1] *= d3_degrees; } }); return { type: "Polygon", coordinates: [ ring ] }; } circle.origin = function(x) { if (!arguments.length) return origin; origin = x; return circle; }; circle.angle = function(x) { if (!arguments.length) return angle; interpolate = d3_geo_circleInterpolate((angle = +x) * d3_radians, precision * d3_radians); return circle; }; circle.precision = function(_) { if (!arguments.length) return precision; interpolate = d3_geo_circleInterpolate(angle * d3_radians, (precision = +_) * d3_radians); return circle; }; return circle.angle(90); }; function d3_geo_circleInterpolate(radius, precision) { var cr = Math.cos(radius), sr = Math.sin(radius); return function(from, to, direction, listener) { var step = direction * precision; if (from != null) { from = d3_geo_circleAngle(cr, from); to = d3_geo_circleAngle(cr, to); if (direction > 0 ? from < to : from > to) from += direction * τ; } else { from = radius + direction * τ; to = radius - .5 * step; } for (var point, t = from; direction > 0 ? t > to : t < to; t -= step) { listener.point((point = d3_geo_spherical([ cr, -sr * Math.cos(t), -sr * Math.sin(t) ]))[0], point[1]); } }; } function d3_geo_circleAngle(cr, point) { var a = d3_geo_cartesian(point); a[0] -= cr; d3_geo_cartesianNormalize(a); var angle = d3_acos(-a[1]); return ((-a[2] < 0 ? -angle : angle) + 2 * Math.PI - ε) % (2 * Math.PI); } d3.geo.distance = function(a, b) { var Δλ = (b[0] - a[0]) * d3_radians, φ0 = a[1] * d3_radians, φ1 = b[1] * d3_radians, sinΔλ = Math.sin(Δλ), cosΔλ = Math.cos(Δλ), sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), sinφ1 = Math.sin(φ1), cosφ1 = Math.cos(φ1), t; return Math.atan2(Math.sqrt((t = cosφ1 * sinΔλ) * t + (t = cosφ0 * sinφ1 - sinφ0 * cosφ1 * cosΔλ) * t), sinφ0 * sinφ1 + cosφ0 * cosφ1 * cosΔλ); }; d3.geo.graticule = function() { var x1, x0, X1, X0, y1, y0, Y1, Y0, dx = 10, dy = dx, DX = 90, DY = 360, x, y, X, Y, precision = 2.5; function graticule() { return { type: "MultiLineString", coordinates: lines() }; } function lines() { return d3.range(Math.ceil(X0 / DX) * DX, X1, DX).map(X).concat(d3.range(Math.ceil(Y0 / DY) * DY, Y1, DY).map(Y)).concat(d3.range(Math.ceil(x0 / dx) * dx, x1, dx).filter(function(x) { return abs(x % DX) > ε; }).map(x)).concat(d3.range(Math.ceil(y0 / dy) * dy, y1, dy).filter(function(y) { return abs(y % DY) > ε; }).map(y)); } graticule.lines = function() { return lines().map(function(coordinates) { return { type: "LineString", coordinates: coordinates }; }); }; graticule.outline = function() { return { type: "Polygon", coordinates: [ X(X0).concat(Y(Y1).slice(1), X(X1).reverse().slice(1), Y(Y0).reverse().slice(1)) ] }; }; graticule.extent = function(_) { if (!arguments.length) return graticule.minorExtent(); return graticule.majorExtent(_).minorExtent(_); }; graticule.majorExtent = function(_) { if (!arguments.length) return [ [ X0, Y0 ], [ X1, Y1 ] ]; X0 = +_[0][0], X1 = +_[1][0]; Y0 = +_[0][1], Y1 = +_[1][1]; if (X0 > X1) _ = X0, X0 = X1, X1 = _; if (Y0 > Y1) _ = Y0, Y0 = Y1, Y1 = _; return graticule.precision(precision); }; graticule.minorExtent = function(_) { if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ]; x0 = +_[0][0], x1 = +_[1][0]; y0 = +_[0][1], y1 = +_[1][1]; if (x0 > x1) _ = x0, x0 = x1, x1 = _; if (y0 > y1) _ = y0, y0 = y1, y1 = _; return graticule.precision(precision); }; graticule.step = function(_) { if (!arguments.length) return graticule.minorStep(); return graticule.majorStep(_).minorStep(_); }; graticule.majorStep = function(_) { if (!arguments.length) return [ DX, DY ]; DX = +_[0], DY = +_[1]; return graticule; }; graticule.minorStep = function(_) { if (!arguments.length) return [ dx, dy ]; dx = +_[0], dy = +_[1]; return graticule; }; graticule.precision = function(_) { if (!arguments.length) return precision; precision = +_; x = d3_geo_graticuleX(y0, y1, 90); y = d3_geo_graticuleY(x0, x1, precision); X = d3_geo_graticuleX(Y0, Y1, 90); Y = d3_geo_graticuleY(X0, X1, precision); return graticule; }; return graticule.majorExtent([ [ -180, -90 + ε ], [ 180, 90 - ε ] ]).minorExtent([ [ -180, -80 - ε ], [ 180, 80 + ε ] ]); }; function d3_geo_graticuleX(y0, y1, dy) { var y = d3.range(y0, y1 - ε, dy).concat(y1); return function(x) { return y.map(function(y) { return [ x, y ]; }); }; } function d3_geo_graticuleY(x0, x1, dx) { var x = d3.range(x0, x1 - ε, dx).concat(x1); return function(y) { return x.map(function(x) { return [ x, y ]; }); }; } function d3_source(d) { return d.source; } function d3_target(d) { return d.target; } d3.geo.greatArc = function() { var source = d3_source, source_, target = d3_target, target_; function greatArc() { return { type: "LineString", coordinates: [ source_ || source.apply(this, arguments), target_ || target.apply(this, arguments) ] }; } greatArc.distance = function() { return d3.geo.distance(source_ || source.apply(this, arguments), target_ || target.apply(this, arguments)); }; greatArc.source = function(_) { if (!arguments.length) return source; source = _, source_ = typeof _ === "function" ? null : _; return greatArc; }; greatArc.target = function(_) { if (!arguments.length) return target; target = _, target_ = typeof _ === "function" ? null : _; return greatArc; }; greatArc.precision = function() { return arguments.length ? greatArc : 0; }; return greatArc; }; d3.geo.interpolate = function(source, target) { return d3_geo_interpolate(source[0] * d3_radians, source[1] * d3_radians, target[0] * d3_radians, target[1] * d3_radians); }; function d3_geo_interpolate(x0, y0, x1, y1) { var cy0 = Math.cos(y0), sy0 = Math.sin(y0), cy1 = Math.cos(y1), sy1 = Math.sin(y1), kx0 = cy0 * Math.cos(x0), ky0 = cy0 * Math.sin(x0), kx1 = cy1 * Math.cos(x1), ky1 = cy1 * Math.sin(x1), d = 2 * Math.asin(Math.sqrt(d3_haversin(y1 - y0) + cy0 * cy1 * d3_haversin(x1 - x0))), k = 1 / Math.sin(d); var interpolate = d ? function(t) { var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1; return [ Math.atan2(y, x) * d3_degrees, Math.atan2(z, Math.sqrt(x * x + y * y)) * d3_degrees ]; } : function() { return [ x0 * d3_degrees, y0 * d3_degrees ]; }; interpolate.distance = d; return interpolate; } d3.geo.length = function(object) { d3_geo_lengthSum = 0; d3.geo.stream(object, d3_geo_length); return d3_geo_lengthSum; }; var d3_geo_lengthSum; var d3_geo_length = { sphere: d3_noop, point: d3_noop, lineStart: d3_geo_lengthLineStart, lineEnd: d3_noop, polygonStart: d3_noop, polygonEnd: d3_noop }; function d3_geo_lengthLineStart() { var λ0, sinφ0, cosφ0; d3_geo_length.point = function(λ, φ) { λ0 = λ * d3_radians, sinφ0 = Math.sin(φ *= d3_radians), cosφ0 = Math.cos(φ); d3_geo_length.point = nextPoint; }; d3_geo_length.lineEnd = function() { d3_geo_length.point = d3_geo_length.lineEnd = d3_noop; }; function nextPoint(λ, φ) { var sinφ = Math.sin(φ *= d3_radians), cosφ = Math.cos(φ), t = abs((λ *= d3_radians) - λ0), cosΔλ = Math.cos(t); d3_geo_lengthSum += Math.atan2(Math.sqrt((t = cosφ * Math.sin(t)) * t + (t = cosφ0 * sinφ - sinφ0 * cosφ * cosΔλ) * t), sinφ0 * sinφ + cosφ0 * cosφ * cosΔλ); λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ; } } function d3_geo_azimuthal(scale, angle) { function azimuthal(λ, φ) { var cosλ = Math.cos(λ), cosφ = Math.cos(φ), k = scale(cosλ * cosφ); return [ k * cosφ * Math.sin(λ), k * Math.sin(φ) ]; } azimuthal.invert = function(x, y) { var ρ = Math.sqrt(x * x + y * y), c = angle(ρ), sinc = Math.sin(c), cosc = Math.cos(c); return [ Math.atan2(x * sinc, ρ * cosc), Math.asin(ρ && y * sinc / ρ) ]; }; return azimuthal; } var d3_geo_azimuthalEqualArea = d3_geo_azimuthal(function(cosλcosφ) { return Math.sqrt(2 / (1 + cosλcosφ)); }, function(ρ) { return 2 * Math.asin(ρ / 2); }); (d3.geo.azimuthalEqualArea = function() { return d3_geo_projection(d3_geo_azimuthalEqualArea); }).raw = d3_geo_azimuthalEqualArea; var d3_geo_azimuthalEquidistant = d3_geo_azimuthal(function(cosλcosφ) { var c = Math.acos(cosλcosφ); return c && c / Math.sin(c); }, d3_identity); (d3.geo.azimuthalEquidistant = function() { return d3_geo_projection(d3_geo_azimuthalEquidistant); }).raw = d3_geo_azimuthalEquidistant; function d3_geo_conicConformal(φ0, φ1) { var cosφ0 = Math.cos(φ0), t = function(φ) { return Math.tan(π / 4 + φ / 2); }, n = φ0 === φ1 ? Math.sin(φ0) : Math.log(cosφ0 / Math.cos(φ1)) / Math.log(t(φ1) / t(φ0)), F = cosφ0 * Math.pow(t(φ0), n) / n; if (!n) return d3_geo_mercator; function forward(λ, φ) { if (F > 0) { if (φ < -halfπ + ε) φ = -halfπ + ε; } else { if (φ > halfπ - ε) φ = halfπ - ε; } var ρ = F / Math.pow(t(φ), n); return [ ρ * Math.sin(n * λ), F - ρ * Math.cos(n * λ) ]; } forward.invert = function(x, y) { var ρ0_y = F - y, ρ = d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y); return [ Math.atan2(x, ρ0_y) / n, 2 * Math.atan(Math.pow(F / ρ, 1 / n)) - halfπ ]; }; return forward; } (d3.geo.conicConformal = function() { return d3_geo_conic(d3_geo_conicConformal); }).raw = d3_geo_conicConformal; function d3_geo_conicEquidistant(φ0, φ1) { var cosφ0 = Math.cos(φ0), n = φ0 === φ1 ? Math.sin(φ0) : (cosφ0 - Math.cos(φ1)) / (φ1 - φ0), G = cosφ0 / n + φ0; if (abs(n) < ε) return d3_geo_equirectangular; function forward(λ, φ) { var ρ = G - φ; return [ ρ * Math.sin(n * λ), G - ρ * Math.cos(n * λ) ]; } forward.invert = function(x, y) { var ρ0_y = G - y; return [ Math.atan2(x, ρ0_y) / n, G - d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y) ]; }; return forward; } (d3.geo.conicEquidistant = function() { return d3_geo_conic(d3_geo_conicEquidistant); }).raw = d3_geo_conicEquidistant; var d3_geo_gnomonic = d3_geo_azimuthal(function(cosλcosφ) { return 1 / cosλcosφ; }, Math.atan); (d3.geo.gnomonic = function() { return d3_geo_projection(d3_geo_gnomonic); }).raw = d3_geo_gnomonic; function d3_geo_mercator(λ, φ) { return [ λ, Math.log(Math.tan(π / 4 + φ / 2)) ]; } d3_geo_mercator.invert = function(x, y) { return [ x, 2 * Math.atan(Math.exp(y)) - halfπ ]; }; function d3_geo_mercatorProjection(project) { var m = d3_geo_projection(project), scale = m.scale, translate = m.translate, clipExtent = m.clipExtent, clipAuto; m.scale = function() { var v = scale.apply(m, arguments); return v === m ? clipAuto ? m.clipExtent(null) : m : v; }; m.translate = function() { var v = translate.apply(m, arguments); return v === m ? clipAuto ? m.clipExtent(null) : m : v; }; m.clipExtent = function(_) { var v = clipExtent.apply(m, arguments); if (v === m) { if (clipAuto = _ == null) { var k = π * scale(), t = translate(); clipExtent([ [ t[0] - k, t[1] - k ], [ t[0] + k, t[1] + k ] ]); } } else if (clipAuto) { v = null; } return v; }; return m.clipExtent(null); } (d3.geo.mercator = function() { return d3_geo_mercatorProjection(d3_geo_mercator); }).raw = d3_geo_mercator; var d3_geo_orthographic = d3_geo_azimuthal(function() { return 1; }, Math.asin); (d3.geo.orthographic = function() { return d3_geo_projection(d3_geo_orthographic); }).raw = d3_geo_orthographic; var d3_geo_stereographic = d3_geo_azimuthal(function(cosλcosφ) { return 1 / (1 + cosλcosφ); }, function(ρ) { return 2 * Math.atan(ρ); }); (d3.geo.stereographic = function() { return d3_geo_projection(d3_geo_stereographic); }).raw = d3_geo_stereographic; function d3_geo_transverseMercator(λ, φ) { return [ Math.log(Math.tan(π / 4 + φ / 2)), -λ ]; } d3_geo_transverseMercator.invert = function(x, y) { return [ -y, 2 * Math.atan(Math.exp(x)) - halfπ ]; }; (d3.geo.transverseMercator = function() { var projection = d3_geo_mercatorProjection(d3_geo_transverseMercator), center = projection.center, rotate = projection.rotate; projection.center = function(_) { return _ ? center([ -_[1], _[0] ]) : (_ = center(), [ _[1], -_[0] ]); }; projection.rotate = function(_) { return _ ? rotate([ _[0], _[1], _.length > 2 ? _[2] + 90 : 90 ]) : (_ = rotate(), [ _[0], _[1], _[2] - 90 ]); }; return rotate([ 0, 0, 90 ]); }).raw = d3_geo_transverseMercator; d3.geom = {}; function d3_geom_pointX(d) { return d[0]; } function d3_geom_pointY(d) { return d[1]; } d3.geom.hull = function(vertices) { var x = d3_geom_pointX, y = d3_geom_pointY; if (arguments.length) return hull(vertices); function hull(data) { if (data.length < 3) return []; var fx = d3_functor(x), fy = d3_functor(y), i, n = data.length, points = [], flippedPoints = []; for (i = 0; i < n; i++) { points.push([ +fx.call(this, data[i], i), +fy.call(this, data[i], i), i ]); } points.sort(d3_geom_hullOrder); for (i = 0; i < n; i++) flippedPoints.push([ points[i][0], -points[i][1] ]); var upper = d3_geom_hullUpper(points), lower = d3_geom_hullUpper(flippedPoints); var skipLeft = lower[0] === upper[0], skipRight = lower[lower.length - 1] === upper[upper.length - 1], polygon = []; for (i = upper.length - 1; i >= 0; --i) polygon.push(data[points[upper[i]][2]]); for (i = +skipLeft; i < lower.length - skipRight; ++i) polygon.push(data[points[lower[i]][2]]); return polygon; } hull.x = function(_) { return arguments.length ? (x = _, hull) : x; }; hull.y = function(_) { return arguments.length ? (y = _, hull) : y; }; return hull; }; function d3_geom_hullUpper(points) { var n = points.length, hull = [ 0, 1 ], hs = 2; for (var i = 2; i < n; i++) { while (hs > 1 && d3_cross2d(points[hull[hs - 2]], points[hull[hs - 1]], points[i]) <= 0) --hs; hull[hs++] = i; } return hull.slice(0, hs); } function d3_geom_hullOrder(a, b) { return a[0] - b[0] || a[1] - b[1]; } d3.geom.polygon = function(coordinates) { d3_subclass(coordinates, d3_geom_polygonPrototype); return coordinates; }; var d3_geom_polygonPrototype = d3.geom.polygon.prototype = []; d3_geom_polygonPrototype.area = function() { var i = -1, n = this.length, a, b = this[n - 1], area = 0; while (++i < n) { a = b; b = this[i]; area += a[1] * b[0] - a[0] * b[1]; } return area * .5; }; d3_geom_polygonPrototype.centroid = function(k) { var i = -1, n = this.length, x = 0, y = 0, a, b = this[n - 1], c; if (!arguments.length) k = -1 / (6 * this.area()); while (++i < n) { a = b; b = this[i]; c = a[0] * b[1] - b[0] * a[1]; x += (a[0] + b[0]) * c; y += (a[1] + b[1]) * c; } return [ x * k, y * k ]; }; d3_geom_polygonPrototype.clip = function(subject) { var input, closed = d3_geom_polygonClosed(subject), i = -1, n = this.length - d3_geom_polygonClosed(this), j, m, a = this[n - 1], b, c, d; while (++i < n) { input = subject.slice(); subject.length = 0; b = this[i]; c = input[(m = input.length - closed) - 1]; j = -1; while (++j < m) { d = input[j]; if (d3_geom_polygonInside(d, a, b)) { if (!d3_geom_polygonInside(c, a, b)) { subject.push(d3_geom_polygonIntersect(c, d, a, b)); } subject.push(d); } else if (d3_geom_polygonInside(c, a, b)) { subject.push(d3_geom_polygonIntersect(c, d, a, b)); } c = d; } if (closed) subject.push(subject[0]); a = b; } return subject; }; function d3_geom_polygonInside(p, a, b) { return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]); } function d3_geom_polygonIntersect(c, d, a, b) { var x1 = c[0], x3 = a[0], x21 = d[0] - x1, x43 = b[0] - x3, y1 = c[1], y3 = a[1], y21 = d[1] - y1, y43 = b[1] - y3, ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21); return [ x1 + ua * x21, y1 + ua * y21 ]; } function d3_geom_polygonClosed(coordinates) { var a = coordinates[0], b = coordinates[coordinates.length - 1]; return !(a[0] - b[0] || a[1] - b[1]); } var d3_geom_voronoiEdges, d3_geom_voronoiCells, d3_geom_voronoiBeaches, d3_geom_voronoiBeachPool = [], d3_geom_voronoiFirstCircle, d3_geom_voronoiCircles, d3_geom_voronoiCirclePool = []; function d3_geom_voronoiBeach() { d3_geom_voronoiRedBlackNode(this); this.edge = this.site = this.circle = null; } function d3_geom_voronoiCreateBeach(site) { var beach = d3_geom_voronoiBeachPool.pop() || new d3_geom_voronoiBeach(); beach.site = site; return beach; } function d3_geom_voronoiDetachBeach(beach) { d3_geom_voronoiDetachCircle(beach); d3_geom_voronoiBeaches.remove(beach); d3_geom_voronoiBeachPool.push(beach); d3_geom_voronoiRedBlackNode(beach); } function d3_geom_voronoiRemoveBeach(beach) { var circle = beach.circle, x = circle.x, y = circle.cy, vertex = { x: x, y: y }, previous = beach.P, next = beach.N, disappearing = [ beach ]; d3_geom_voronoiDetachBeach(beach); var lArc = previous; while (lArc.circle && abs(x - lArc.circle.x) < ε && abs(y - lArc.circle.cy) < ε) { previous = lArc.P; disappearing.unshift(lArc); d3_geom_voronoiDetachBeach(lArc); lArc = previous; } disappearing.unshift(lArc); d3_geom_voronoiDetachCircle(lArc); var rArc = next; while (rArc.circle && abs(x - rArc.circle.x) < ε && abs(y - rArc.circle.cy) < ε) { next = rArc.N; disappearing.push(rArc); d3_geom_voronoiDetachBeach(rArc); rArc = next; } disappearing.push(rArc); d3_geom_voronoiDetachCircle(rArc); var nArcs = disappearing.length, iArc; for (iArc = 1; iArc < nArcs; ++iArc) { rArc = disappearing[iArc]; lArc = disappearing[iArc - 1]; d3_geom_voronoiSetEdgeEnd(rArc.edge, lArc.site, rArc.site, vertex); } lArc = disappearing[0]; rArc = disappearing[nArcs - 1]; rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, rArc.site, null, vertex); d3_geom_voronoiAttachCircle(lArc); d3_geom_voronoiAttachCircle(rArc); } function d3_geom_voronoiAddBeach(site) { var x = site.x, directrix = site.y, lArc, rArc, dxl, dxr, node = d3_geom_voronoiBeaches._; while (node) { dxl = d3_geom_voronoiLeftBreakPoint(node, directrix) - x; if (dxl > ε) node = node.L; else { dxr = x - d3_geom_voronoiRightBreakPoint(node, directrix); if (dxr > ε) { if (!node.R) { lArc = node; break; } node = node.R; } else { if (dxl > -ε) { lArc = node.P; rArc = node; } else if (dxr > -ε) { lArc = node; rArc = node.N; } else { lArc = rArc = node; } break; } } } var newArc = d3_geom_voronoiCreateBeach(site); d3_geom_voronoiBeaches.insert(lArc, newArc); if (!lArc && !rArc) return; if (lArc === rArc) { d3_geom_voronoiDetachCircle(lArc); rArc = d3_geom_voronoiCreateBeach(lArc.site); d3_geom_voronoiBeaches.insert(newArc, rArc); newArc.edge = rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site); d3_geom_voronoiAttachCircle(lArc); d3_geom_voronoiAttachCircle(rArc); return; } if (!rArc) { newArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site); return; } d3_geom_voronoiDetachCircle(lArc); d3_geom_voronoiDetachCircle(rArc); var lSite = lArc.site, ax = lSite.x, ay = lSite.y, bx = site.x - ax, by = site.y - ay, rSite = rArc.site, cx = rSite.x - ax, cy = rSite.y - ay, d = 2 * (bx * cy - by * cx), hb = bx * bx + by * by, hc = cx * cx + cy * cy, vertex = { x: (cy * hb - by * hc) / d + ax, y: (bx * hc - cx * hb) / d + ay }; d3_geom_voronoiSetEdgeEnd(rArc.edge, lSite, rSite, vertex); newArc.edge = d3_geom_voronoiCreateEdge(lSite, site, null, vertex); rArc.edge = d3_geom_voronoiCreateEdge(site, rSite, null, vertex); d3_geom_voronoiAttachCircle(lArc); d3_geom_voronoiAttachCircle(rArc); } function d3_geom_voronoiLeftBreakPoint(arc, directrix) { var site = arc.site, rfocx = site.x, rfocy = site.y, pby2 = rfocy - directrix; if (!pby2) return rfocx; var lArc = arc.P; if (!lArc) return -Infinity; site = lArc.site; var lfocx = site.x, lfocy = site.y, plby2 = lfocy - directrix; if (!plby2) return lfocx; var hl = lfocx - rfocx, aby2 = 1 / pby2 - 1 / plby2, b = hl / plby2; if (aby2) return (-b + Math.sqrt(b * b - 2 * aby2 * (hl * hl / (-2 * plby2) - lfocy + plby2 / 2 + rfocy - pby2 / 2))) / aby2 + rfocx; return (rfocx + lfocx) / 2; } function d3_geom_voronoiRightBreakPoint(arc, directrix) { var rArc = arc.N; if (rArc) return d3_geom_voronoiLeftBreakPoint(rArc, directrix); var site = arc.site; return site.y === directrix ? site.x : Infinity; } function d3_geom_voronoiCell(site) { this.site = site; this.edges = []; } d3_geom_voronoiCell.prototype.prepare = function() { var halfEdges = this.edges, iHalfEdge = halfEdges.length, edge; while (iHalfEdge--) { edge = halfEdges[iHalfEdge].edge; if (!edge.b || !edge.a) halfEdges.splice(iHalfEdge, 1); } halfEdges.sort(d3_geom_voronoiHalfEdgeOrder); return halfEdges.length; }; function d3_geom_voronoiCloseCells(extent) { var x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], x2, y2, x3, y3, cells = d3_geom_voronoiCells, iCell = cells.length, cell, iHalfEdge, halfEdges, nHalfEdges, start, end; while (iCell--) { cell = cells[iCell]; if (!cell || !cell.prepare()) continue; halfEdges = cell.edges; nHalfEdges = halfEdges.length; iHalfEdge = 0; while (iHalfEdge < nHalfEdges) { end = halfEdges[iHalfEdge].end(), x3 = end.x, y3 = end.y; start = halfEdges[++iHalfEdge % nHalfEdges].start(), x2 = start.x, y2 = start.y; if (abs(x3 - x2) > ε || abs(y3 - y2) > ε) { halfEdges.splice(iHalfEdge, 0, new d3_geom_voronoiHalfEdge(d3_geom_voronoiCreateBorderEdge(cell.site, end, abs(x3 - x0) < ε && y1 - y3 > ε ? { x: x0, y: abs(x2 - x0) < ε ? y2 : y1 } : abs(y3 - y1) < ε && x1 - x3 > ε ? { x: abs(y2 - y1) < ε ? x2 : x1, y: y1 } : abs(x3 - x1) < ε && y3 - y0 > ε ? { x: x1, y: abs(x2 - x1) < ε ? y2 : y0 } : abs(y3 - y0) < ε && x3 - x0 > ε ? { x: abs(y2 - y0) < ε ? x2 : x0, y: y0 } : null), cell.site, null)); ++nHalfEdges; } } } } function d3_geom_voronoiHalfEdgeOrder(a, b) { return b.angle - a.angle; } function d3_geom_voronoiCircle() { d3_geom_voronoiRedBlackNode(this); this.x = this.y = this.arc = this.site = this.cy = null; } function d3_geom_voronoiAttachCircle(arc) { var lArc = arc.P, rArc = arc.N; if (!lArc || !rArc) return; var lSite = lArc.site, cSite = arc.site, rSite = rArc.site; if (lSite === rSite) return; var bx = cSite.x, by = cSite.y, ax = lSite.x - bx, ay = lSite.y - by, cx = rSite.x - bx, cy = rSite.y - by; var d = 2 * (ax * cy - ay * cx); if (d >= -ε2) return; var ha = ax * ax + ay * ay, hc = cx * cx + cy * cy, x = (cy * ha - ay * hc) / d, y = (ax * hc - cx * ha) / d, cy = y + by; var circle = d3_geom_voronoiCirclePool.pop() || new d3_geom_voronoiCircle(); circle.arc = arc; circle.site = cSite; circle.x = x + bx; circle.y = cy + Math.sqrt(x * x + y * y); circle.cy = cy; arc.circle = circle; var before = null, node = d3_geom_voronoiCircles._; while (node) { if (circle.y < node.y || circle.y === node.y && circle.x <= node.x) { if (node.L) node = node.L; else { before = node.P; break; } } else { if (node.R) node = node.R; else { before = node; break; } } } d3_geom_voronoiCircles.insert(before, circle); if (!before) d3_geom_voronoiFirstCircle = circle; } function d3_geom_voronoiDetachCircle(arc) { var circle = arc.circle; if (circle) { if (!circle.P) d3_geom_voronoiFirstCircle = circle.N; d3_geom_voronoiCircles.remove(circle); d3_geom_voronoiCirclePool.push(circle); d3_geom_voronoiRedBlackNode(circle); arc.circle = null; } } function d3_geom_voronoiClipEdges(extent) { var edges = d3_geom_voronoiEdges, clip = d3_geom_clipLine(extent[0][0], extent[0][1], extent[1][0], extent[1][1]), i = edges.length, e; while (i--) { e = edges[i]; if (!d3_geom_voronoiConnectEdge(e, extent) || !clip(e) || abs(e.a.x - e.b.x) < ε && abs(e.a.y - e.b.y) < ε) { e.a = e.b = null; edges.splice(i, 1); } } } function d3_geom_voronoiConnectEdge(edge, extent) { var vb = edge.b; if (vb) return true; var va = edge.a, x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], lSite = edge.l, rSite = edge.r, lx = lSite.x, ly = lSite.y, rx = rSite.x, ry = rSite.y, fx = (lx + rx) / 2, fy = (ly + ry) / 2, fm, fb; if (ry === ly) { if (fx < x0 || fx >= x1) return; if (lx > rx) { if (!va) va = { x: fx, y: y0 }; else if (va.y >= y1) return; vb = { x: fx, y: y1 }; } else { if (!va) va = { x: fx, y: y1 }; else if (va.y < y0) return; vb = { x: fx, y: y0 }; } } else { fm = (lx - rx) / (ry - ly); fb = fy - fm * fx; if (fm < -1 || fm > 1) { if (lx > rx) { if (!va) va = { x: (y0 - fb) / fm, y: y0 }; else if (va.y >= y1) return; vb = { x: (y1 - fb) / fm, y: y1 }; } else { if (!va) va = { x: (y1 - fb) / fm, y: y1 }; else if (va.y < y0) return; vb = { x: (y0 - fb) / fm, y: y0 }; } } else { if (ly < ry) { if (!va) va = { x: x0, y: fm * x0 + fb }; else if (va.x >= x1) return; vb = { x: x1, y: fm * x1 + fb }; } else { if (!va) va = { x: x1, y: fm * x1 + fb }; else if (va.x < x0) return; vb = { x: x0, y: fm * x0 + fb }; } } } edge.a = va; edge.b = vb; return true; } function d3_geom_voronoiEdge(lSite, rSite) { this.l = lSite; this.r = rSite; this.a = this.b = null; } function d3_geom_voronoiCreateEdge(lSite, rSite, va, vb) { var edge = new d3_geom_voronoiEdge(lSite, rSite); d3_geom_voronoiEdges.push(edge); if (va) d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, va); if (vb) d3_geom_voronoiSetEdgeEnd(edge, rSite, lSite, vb); d3_geom_voronoiCells[lSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, lSite, rSite)); d3_geom_voronoiCells[rSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, rSite, lSite)); return edge; } function d3_geom_voronoiCreateBorderEdge(lSite, va, vb) { var edge = new d3_geom_voronoiEdge(lSite, null); edge.a = va; edge.b = vb; d3_geom_voronoiEdges.push(edge); return edge; } function d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, vertex) { if (!edge.a && !edge.b) { edge.a = vertex; edge.l = lSite; edge.r = rSite; } else if (edge.l === rSite) { edge.b = vertex; } else { edge.a = vertex; } } function d3_geom_voronoiHalfEdge(edge, lSite, rSite) { var va = edge.a, vb = edge.b; this.edge = edge; this.site = lSite; this.angle = rSite ? Math.atan2(rSite.y - lSite.y, rSite.x - lSite.x) : edge.l === lSite ? Math.atan2(vb.x - va.x, va.y - vb.y) : Math.atan2(va.x - vb.x, vb.y - va.y); } d3_geom_voronoiHalfEdge.prototype = { start: function() { return this.edge.l === this.site ? this.edge.a : this.edge.b; }, end: function() { return this.edge.l === this.site ? this.edge.b : this.edge.a; } }; function d3_geom_voronoiRedBlackTree() { this._ = null; } function d3_geom_voronoiRedBlackNode(node) { node.U = node.C = node.L = node.R = node.P = node.N = null; } d3_geom_voronoiRedBlackTree.prototype = { insert: function(after, node) { var parent, grandpa, uncle; if (after) { node.P = after; node.N = after.N; if (after.N) after.N.P = node; after.N = node; if (after.R) { after = after.R; while (after.L) after = after.L; after.L = node; } else { after.R = node; } parent = after; } else if (this._) { after = d3_geom_voronoiRedBlackFirst(this._); node.P = null; node.N = after; after.P = after.L = node; parent = after; } else { node.P = node.N = null; this._ = node; parent = null; } node.L = node.R = null; node.U = parent; node.C = true; after = node; while (parent && parent.C) { grandpa = parent.U; if (parent === grandpa.L) { uncle = grandpa.R; if (uncle && uncle.C) { parent.C = uncle.C = false; grandpa.C = true; after = grandpa; } else { if (after === parent.R) { d3_geom_voronoiRedBlackRotateLeft(this, parent); after = parent; parent = after.U; } parent.C = false; grandpa.C = true; d3_geom_voronoiRedBlackRotateRight(this, grandpa); } } else { uncle = grandpa.L; if (uncle && uncle.C) { parent.C = uncle.C = false; grandpa.C = true; after = grandpa; } else { if (after === parent.L) { d3_geom_voronoiRedBlackRotateRight(this, parent); after = parent; parent = after.U; } parent.C = false; grandpa.C = true; d3_geom_voronoiRedBlackRotateLeft(this, grandpa); } } parent = after.U; } this._.C = false; }, remove: function(node) { if (node.N) node.N.P = node.P; if (node.P) node.P.N = node.N; node.N = node.P = null; var parent = node.U, sibling, left = node.L, right = node.R, next, red; if (!left) next = right; else if (!right) next = left; else next = d3_geom_voronoiRedBlackFirst(right); if (parent) { if (parent.L === node) parent.L = next; else parent.R = next; } else { this._ = next; } if (left && right) { red = next.C; next.C = node.C; next.L = left; left.U = next; if (next !== right) { parent = next.U; next.U = node.U; node = next.R; parent.L = node; next.R = right; right.U = next; } else { next.U = parent; parent = next; node = next.R; } } else { red = node.C; node = next; } if (node) node.U = parent; if (red) return; if (node && node.C) { node.C = false; return; } do { if (node === this._) break; if (node === parent.L) { sibling = parent.R; if (sibling.C) { sibling.C = false; parent.C = true; d3_geom_voronoiRedBlackRotateLeft(this, parent); sibling = parent.R; } if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) { if (!sibling.R || !sibling.R.C) { sibling.L.C = false; sibling.C = true; d3_geom_voronoiRedBlackRotateRight(this, sibling); sibling = parent.R; } sibling.C = parent.C; parent.C = sibling.R.C = false; d3_geom_voronoiRedBlackRotateLeft(this, parent); node = this._; break; } } else { sibling = parent.L; if (sibling.C) { sibling.C = false; parent.C = true; d3_geom_voronoiRedBlackRotateRight(this, parent); sibling = parent.L; } if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) { if (!sibling.L || !sibling.L.C) { sibling.R.C = false; sibling.C = true; d3_geom_voronoiRedBlackRotateLeft(this, sibling); sibling = parent.L; } sibling.C = parent.C; parent.C = sibling.L.C = false; d3_geom_voronoiRedBlackRotateRight(this, parent); node = this._; break; } } sibling.C = true; node = parent; parent = parent.U; } while (!node.C); if (node) node.C = false; } }; function d3_geom_voronoiRedBlackRotateLeft(tree, node) { var p = node, q = node.R, parent = p.U; if (parent) { if (parent.L === p) parent.L = q; else parent.R = q; } else { tree._ = q; } q.U = parent; p.U = q; p.R = q.L; if (p.R) p.R.U = p; q.L = p; } function d3_geom_voronoiRedBlackRotateRight(tree, node) { var p = node, q = node.L, parent = p.U; if (parent) { if (parent.L === p) parent.L = q; else parent.R = q; } else { tree._ = q; } q.U = parent; p.U = q; p.L = q.R; if (p.L) p.L.U = p; q.R = p; } function d3_geom_voronoiRedBlackFirst(node) { while (node.L) node = node.L; return node; } function d3_geom_voronoi(sites, bbox) { var site = sites.sort(d3_geom_voronoiVertexOrder).pop(), x0, y0, circle; d3_geom_voronoiEdges = []; d3_geom_voronoiCells = new Array(sites.length); d3_geom_voronoiBeaches = new d3_geom_voronoiRedBlackTree(); d3_geom_voronoiCircles = new d3_geom_voronoiRedBlackTree(); while (true) { circle = d3_geom_voronoiFirstCircle; if (site && (!circle || site.y < circle.y || site.y === circle.y && site.x < circle.x)) { if (site.x !== x0 || site.y !== y0) { d3_geom_voronoiCells[site.i] = new d3_geom_voronoiCell(site); d3_geom_voronoiAddBeach(site); x0 = site.x, y0 = site.y; } site = sites.pop(); } else if (circle) { d3_geom_voronoiRemoveBeach(circle.arc); } else { break; } } if (bbox) d3_geom_voronoiClipEdges(bbox), d3_geom_voronoiCloseCells(bbox); var diagram = { cells: d3_geom_voronoiCells, edges: d3_geom_voronoiEdges }; d3_geom_voronoiBeaches = d3_geom_voronoiCircles = d3_geom_voronoiEdges = d3_geom_voronoiCells = null; return diagram; } function d3_geom_voronoiVertexOrder(a, b) { return b.y - a.y || b.x - a.x; } d3.geom.voronoi = function(points) { var x = d3_geom_pointX, y = d3_geom_pointY, fx = x, fy = y, clipExtent = d3_geom_voronoiClipExtent; if (points) return voronoi(points); function voronoi(data) { var polygons = new Array(data.length), x0 = clipExtent[0][0], y0 = clipExtent[0][1], x1 = clipExtent[1][0], y1 = clipExtent[1][1]; d3_geom_voronoi(sites(data), clipExtent).cells.forEach(function(cell, i) { var edges = cell.edges, site = cell.site, polygon = polygons[i] = edges.length ? edges.map(function(e) { var s = e.start(); return [ s.x, s.y ]; }) : site.x >= x0 && site.x <= x1 && site.y >= y0 && site.y <= y1 ? [ [ x0, y1 ], [ x1, y1 ], [ x1, y0 ], [ x0, y0 ] ] : []; polygon.point = data[i]; }); return polygons; } function sites(data) { return data.map(function(d, i) { return { x: Math.round(fx(d, i) / ε) * ε, y: Math.round(fy(d, i) / ε) * ε, i: i }; }); } voronoi.links = function(data) { return d3_geom_voronoi(sites(data)).edges.filter(function(edge) { return edge.l && edge.r; }).map(function(edge) { return { source: data[edge.l.i], target: data[edge.r.i] }; }); }; voronoi.triangles = function(data) { var triangles = []; d3_geom_voronoi(sites(data)).cells.forEach(function(cell, i) { var site = cell.site, edges = cell.edges.sort(d3_geom_voronoiHalfEdgeOrder), j = -1, m = edges.length, e0, s0, e1 = edges[m - 1].edge, s1 = e1.l === site ? e1.r : e1.l; while (++j < m) { e0 = e1; s0 = s1; e1 = edges[j].edge; s1 = e1.l === site ? e1.r : e1.l; if (i < s0.i && i < s1.i && d3_geom_voronoiTriangleArea(site, s0, s1) < 0) { triangles.push([ data[i], data[s0.i], data[s1.i] ]); } } }); return triangles; }; voronoi.x = function(_) { return arguments.length ? (fx = d3_functor(x = _), voronoi) : x; }; voronoi.y = function(_) { return arguments.length ? (fy = d3_functor(y = _), voronoi) : y; }; voronoi.clipExtent = function(_) { if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent; clipExtent = _ == null ? d3_geom_voronoiClipExtent : _; return voronoi; }; voronoi.size = function(_) { if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent && clipExtent[1]; return voronoi.clipExtent(_ && [ [ 0, 0 ], _ ]); }; return voronoi; }; var d3_geom_voronoiClipExtent = [ [ -1e6, -1e6 ], [ 1e6, 1e6 ] ]; function d3_geom_voronoiTriangleArea(a, b, c) { return (a.x - c.x) * (b.y - a.y) - (a.x - b.x) * (c.y - a.y); } d3.geom.delaunay = function(vertices) { return d3.geom.voronoi().triangles(vertices); }; d3.geom.quadtree = function(points, x1, y1, x2, y2) { var x = d3_geom_pointX, y = d3_geom_pointY, compat; if (compat = arguments.length) { x = d3_geom_quadtreeCompatX; y = d3_geom_quadtreeCompatY; if (compat === 3) { y2 = y1; x2 = x1; y1 = x1 = 0; } return quadtree(points); } function quadtree(data) { var d, fx = d3_functor(x), fy = d3_functor(y), xs, ys, i, n, x1_, y1_, x2_, y2_; if (x1 != null) { x1_ = x1, y1_ = y1, x2_ = x2, y2_ = y2; } else { x2_ = y2_ = -(x1_ = y1_ = Infinity); xs = [], ys = []; n = data.length; if (compat) for (i = 0; i < n; ++i) { d = data[i]; if (d.x < x1_) x1_ = d.x; if (d.y < y1_) y1_ = d.y; if (d.x > x2_) x2_ = d.x; if (d.y > y2_) y2_ = d.y; xs.push(d.x); ys.push(d.y); } else for (i = 0; i < n; ++i) { var x_ = +fx(d = data[i], i), y_ = +fy(d, i); if (x_ < x1_) x1_ = x_; if (y_ < y1_) y1_ = y_; if (x_ > x2_) x2_ = x_; if (y_ > y2_) y2_ = y_; xs.push(x_); ys.push(y_); } } var dx = x2_ - x1_, dy = y2_ - y1_; if (dx > dy) y2_ = y1_ + dx; else x2_ = x1_ + dy; function insert(n, d, x, y, x1, y1, x2, y2) { if (isNaN(x) || isNaN(y)) return; if (n.leaf) { var nx = n.x, ny = n.y; if (nx != null) { if (abs(nx - x) + abs(ny - y) < .01) { insertChild(n, d, x, y, x1, y1, x2, y2); } else { var nPoint = n.point; n.x = n.y = n.point = null; insertChild(n, nPoint, nx, ny, x1, y1, x2, y2); insertChild(n, d, x, y, x1, y1, x2, y2); } } else { n.x = x, n.y = y, n.point = d; } } else { insertChild(n, d, x, y, x1, y1, x2, y2); } } function insertChild(n, d, x, y, x1, y1, x2, y2) { var xm = (x1 + x2) * .5, ym = (y1 + y2) * .5, right = x >= xm, below = y >= ym, i = below << 1 | right; n.leaf = false; n = n.nodes[i] || (n.nodes[i] = d3_geom_quadtreeNode()); if (right) x1 = xm; else x2 = xm; if (below) y1 = ym; else y2 = ym; insert(n, d, x, y, x1, y1, x2, y2); } var root = d3_geom_quadtreeNode(); root.add = function(d) { insert(root, d, +fx(d, ++i), +fy(d, i), x1_, y1_, x2_, y2_); }; root.visit = function(f) { d3_geom_quadtreeVisit(f, root, x1_, y1_, x2_, y2_); }; root.find = function(point) { return d3_geom_quadtreeFind(root, point[0], point[1], x1_, y1_, x2_, y2_); }; i = -1; if (x1 == null) { while (++i < n) { insert(root, data[i], xs[i], ys[i], x1_, y1_, x2_, y2_); } --i; } else data.forEach(root.add); xs = ys = data = d = null; return root; } quadtree.x = function(_) { return arguments.length ? (x = _, quadtree) : x; }; quadtree.y = function(_) { return arguments.length ? (y = _, quadtree) : y; }; quadtree.extent = function(_) { if (!arguments.length) return x1 == null ? null : [ [ x1, y1 ], [ x2, y2 ] ]; if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = +_[0][0], y1 = +_[0][1], x2 = +_[1][0], y2 = +_[1][1]; return quadtree; }; quadtree.size = function(_) { if (!arguments.length) return x1 == null ? null : [ x2 - x1, y2 - y1 ]; if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = y1 = 0, x2 = +_[0], y2 = +_[1]; return quadtree; }; return quadtree; }; function d3_geom_quadtreeCompatX(d) { return d.x; } function d3_geom_quadtreeCompatY(d) { return d.y; } function d3_geom_quadtreeNode() { return { leaf: true, nodes: [], point: null, x: null, y: null }; } function d3_geom_quadtreeVisit(f, node, x1, y1, x2, y2) { if (!f(node, x1, y1, x2, y2)) { var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, children = node.nodes; if (children[0]) d3_geom_quadtreeVisit(f, children[0], x1, y1, sx, sy); if (children[1]) d3_geom_quadtreeVisit(f, children[1], sx, y1, x2, sy); if (children[2]) d3_geom_quadtreeVisit(f, children[2], x1, sy, sx, y2); if (children[3]) d3_geom_quadtreeVisit(f, children[3], sx, sy, x2, y2); } } function d3_geom_quadtreeFind(root, x, y, x0, y0, x3, y3) { var minDistance2 = Infinity, closestPoint; (function find(node, x1, y1, x2, y2) { if (x1 > x3 || y1 > y3 || x2 < x0 || y2 < y0) return; if (point = node.point) { var point, dx = x - point[0], dy = y - point[1], distance2 = dx * dx + dy * dy; if (distance2 < minDistance2) { var distance = Math.sqrt(minDistance2 = distance2); x0 = x - distance, y0 = y - distance; x3 = x + distance, y3 = y + distance; closestPoint = point; } } var children = node.nodes, xm = (x1 + x2) * .5, ym = (y1 + y2) * .5, right = x >= xm, below = y >= ym; for (var i = below << 1 | right, j = i + 4; i < j; ++i) { if (node = children[i & 3]) switch (i & 3) { case 0: find(node, x1, y1, xm, ym); break; case 1: find(node, xm, y1, x2, ym); break; case 2: find(node, x1, ym, xm, y2); break; case 3: find(node, xm, ym, x2, y2); break; } } })(root, x0, y0, x3, y3); return closestPoint; } d3.interpolateRgb = d3_interpolateRgb; function d3_interpolateRgb(a, b) { a = d3.rgb(a); b = d3.rgb(b); var ar = a.r, ag = a.g, ab = a.b, br = b.r - ar, bg = b.g - ag, bb = b.b - ab; return function(t) { return "#" + d3_rgb_hex(Math.round(ar + br * t)) + d3_rgb_hex(Math.round(ag + bg * t)) + d3_rgb_hex(Math.round(ab + bb * t)); }; } d3.interpolateObject = d3_interpolateObject; function d3_interpolateObject(a, b) { var i = {}, c = {}, k; for (k in a) { if (k in b) { i[k] = d3_interpolate(a[k], b[k]); } else { c[k] = a[k]; } } for (k in b) { if (!(k in a)) { c[k] = b[k]; } } return function(t) { for (k in i) c[k] = i[k](t); return c; }; } d3.interpolateNumber = d3_interpolateNumber; function d3_interpolateNumber(a, b) { a = +a, b = +b; return function(t) { return a * (1 - t) + b * t; }; } d3.interpolateString = d3_interpolateString; function d3_interpolateString(a, b) { var bi = d3_interpolate_numberA.lastIndex = d3_interpolate_numberB.lastIndex = 0, am, bm, bs, i = -1, s = [], q = []; a = a + "", b = b + ""; while ((am = d3_interpolate_numberA.exec(a)) && (bm = d3_interpolate_numberB.exec(b))) { if ((bs = bm.index) > bi) { bs = b.slice(bi, bs); if (s[i]) s[i] += bs; else s[++i] = bs; } if ((am = am[0]) === (bm = bm[0])) { if (s[i]) s[i] += bm; else s[++i] = bm; } else { s[++i] = null; q.push({ i: i, x: d3_interpolateNumber(am, bm) }); } bi = d3_interpolate_numberB.lastIndex; } if (bi < b.length) { bs = b.slice(bi); if (s[i]) s[i] += bs; else s[++i] = bs; } return s.length < 2 ? q[0] ? (b = q[0].x, function(t) { return b(t) + ""; }) : function() { return b; } : (b = q.length, function(t) { for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t); return s.join(""); }); } var d3_interpolate_numberA = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g, d3_interpolate_numberB = new RegExp(d3_interpolate_numberA.source, "g"); d3.interpolate = d3_interpolate; function d3_interpolate(a, b) { var i = d3.interpolators.length, f; while (--i >= 0 && !(f = d3.interpolators[i](a, b))) ; return f; } d3.interpolators = [ function(a, b) { var t = typeof b; return (t === "string" ? d3_rgb_names.has(b) || /^(#|rgb\(|hsl\()/.test(b) ? d3_interpolateRgb : d3_interpolateString : b instanceof d3_color ? d3_interpolateRgb : Array.isArray(b) ? d3_interpolateArray : t === "object" && isNaN(b) ? d3_interpolateObject : d3_interpolateNumber)(a, b); } ]; d3.interpolateArray = d3_interpolateArray; function d3_interpolateArray(a, b) { var x = [], c = [], na = a.length, nb = b.length, n0 = Math.min(a.length, b.length), i; for (i = 0; i < n0; ++i) x.push(d3_interpolate(a[i], b[i])); for (;i < na; ++i) c[i] = a[i]; for (;i < nb; ++i) c[i] = b[i]; return function(t) { for (i = 0; i < n0; ++i) c[i] = x[i](t); return c; }; } var d3_ease_default = function() { return d3_identity; }; var d3_ease = d3.map({ linear: d3_ease_default, poly: d3_ease_poly, quad: function() { return d3_ease_quad; }, cubic: function() { return d3_ease_cubic; }, sin: function() { return d3_ease_sin; }, exp: function() { return d3_ease_exp; }, circle: function() { return d3_ease_circle; }, elastic: d3_ease_elastic, back: d3_ease_back, bounce: function() { return d3_ease_bounce; } }); var d3_ease_mode = d3.map({ "in": d3_identity, out: d3_ease_reverse, "in-out": d3_ease_reflect, "out-in": function(f) { return d3_ease_reflect(d3_ease_reverse(f)); } }); d3.ease = function(name) { var i = name.indexOf("-"), t = i >= 0 ? name.slice(0, i) : name, m = i >= 0 ? name.slice(i + 1) : "in"; t = d3_ease.get(t) || d3_ease_default; m = d3_ease_mode.get(m) || d3_identity; return d3_ease_clamp(m(t.apply(null, d3_arraySlice.call(arguments, 1)))); }; function d3_ease_clamp(f) { return function(t) { return t <= 0 ? 0 : t >= 1 ? 1 : f(t); }; } function d3_ease_reverse(f) { return function(t) { return 1 - f(1 - t); }; } function d3_ease_reflect(f) { return function(t) { return .5 * (t < .5 ? f(2 * t) : 2 - f(2 - 2 * t)); }; } function d3_ease_quad(t) { return t * t; } function d3_ease_cubic(t) { return t * t * t; } function d3_ease_cubicInOut(t) { if (t <= 0) return 0; if (t >= 1) return 1; var t2 = t * t, t3 = t2 * t; return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75); } function d3_ease_poly(e) { return function(t) { return Math.pow(t, e); }; } function d3_ease_sin(t) { return 1 - Math.cos(t * halfπ); } function d3_ease_exp(t) { return Math.pow(2, 10 * (t - 1)); } function d3_ease_circle(t) { return 1 - Math.sqrt(1 - t * t); } function d3_ease_elastic(a, p) { var s; if (arguments.length < 2) p = .45; if (arguments.length) s = p / τ * Math.asin(1 / a); else a = 1, s = p / 4; return function(t) { return 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * τ / p); }; } function d3_ease_back(s) { if (!s) s = 1.70158; return function(t) { return t * t * ((s + 1) * t - s); }; } function d3_ease_bounce(t) { return t < 1 / 2.75 ? 7.5625 * t * t : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + .75 : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + .9375 : 7.5625 * (t -= 2.625 / 2.75) * t + .984375; } d3.interpolateHcl = d3_interpolateHcl; function d3_interpolateHcl(a, b) { a = d3.hcl(a); b = d3.hcl(b); var ah = a.h, ac = a.c, al = a.l, bh = b.h - ah, bc = b.c - ac, bl = b.l - al; if (isNaN(bc)) bc = 0, ac = isNaN(ac) ? b.c : ac; if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; return function(t) { return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + ""; }; } d3.interpolateHsl = d3_interpolateHsl; function d3_interpolateHsl(a, b) { a = d3.hsl(a); b = d3.hsl(b); var ah = a.h, as = a.s, al = a.l, bh = b.h - ah, bs = b.s - as, bl = b.l - al; if (isNaN(bs)) bs = 0, as = isNaN(as) ? b.s : as; if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; return function(t) { return d3_hsl_rgb(ah + bh * t, as + bs * t, al + bl * t) + ""; }; } d3.interpolateLab = d3_interpolateLab; function d3_interpolateLab(a, b) { a = d3.lab(a); b = d3.lab(b); var al = a.l, aa = a.a, ab = a.b, bl = b.l - al, ba = b.a - aa, bb = b.b - ab; return function(t) { return d3_lab_rgb(al + bl * t, aa + ba * t, ab + bb * t) + ""; }; } d3.interpolateRound = d3_interpolateRound; function d3_interpolateRound(a, b) { b -= a; return function(t) { return Math.round(a + b * t); }; } d3.transform = function(string) { var g = d3_document.createElementNS(d3.ns.prefix.svg, "g"); return (d3.transform = function(string) { if (string != null) { g.setAttribute("transform", string); var t = g.transform.baseVal.consolidate(); } return new d3_transform(t ? t.matrix : d3_transformIdentity); })(string); }; function d3_transform(m) { var r0 = [ m.a, m.b ], r1 = [ m.c, m.d ], kx = d3_transformNormalize(r0), kz = d3_transformDot(r0, r1), ky = d3_transformNormalize(d3_transformCombine(r1, r0, -kz)) || 0; if (r0[0] * r1[1] < r1[0] * r0[1]) { r0[0] *= -1; r0[1] *= -1; kx *= -1; kz *= -1; } this.rotate = (kx ? Math.atan2(r0[1], r0[0]) : Math.atan2(-r1[0], r1[1])) * d3_degrees; this.translate = [ m.e, m.f ]; this.scale = [ kx, ky ]; this.skew = ky ? Math.atan2(kz, ky) * d3_degrees : 0; } d3_transform.prototype.toString = function() { return "translate(" + this.translate + ")rotate(" + this.rotate + ")skewX(" + this.skew + ")scale(" + this.scale + ")"; }; function d3_transformDot(a, b) { return a[0] * b[0] + a[1] * b[1]; } function d3_transformNormalize(a) { var k = Math.sqrt(d3_transformDot(a, a)); if (k) { a[0] /= k; a[1] /= k; } return k; } function d3_transformCombine(a, b, k) { a[0] += k * b[0]; a[1] += k * b[1]; return a; } var d3_transformIdentity = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }; d3.interpolateTransform = d3_interpolateTransform; function d3_interpolateTransform(a, b) { var s = [], q = [], n, A = d3.transform(a), B = d3.transform(b), ta = A.translate, tb = B.translate, ra = A.rotate, rb = B.rotate, wa = A.skew, wb = B.skew, ka = A.scale, kb = B.scale; if (ta[0] != tb[0] || ta[1] != tb[1]) { s.push("translate(", null, ",", null, ")"); q.push({ i: 1, x: d3_interpolateNumber(ta[0], tb[0]) }, { i: 3, x: d3_interpolateNumber(ta[1], tb[1]) }); } else if (tb[0] || tb[1]) { s.push("translate(" + tb + ")"); } else { s.push(""); } if (ra != rb) { if (ra - rb > 180) rb += 360; else if (rb - ra > 180) ra += 360; q.push({ i: s.push(s.pop() + "rotate(", null, ")") - 2, x: d3_interpolateNumber(ra, rb) }); } else if (rb) { s.push(s.pop() + "rotate(" + rb + ")"); } if (wa != wb) { q.push({ i: s.push(s.pop() + "skewX(", null, ")") - 2, x: d3_interpolateNumber(wa, wb) }); } else if (wb) { s.push(s.pop() + "skewX(" + wb + ")"); } if (ka[0] != kb[0] || ka[1] != kb[1]) { n = s.push(s.pop() + "scale(", null, ",", null, ")"); q.push({ i: n - 4, x: d3_interpolateNumber(ka[0], kb[0]) }, { i: n - 2, x: d3_interpolateNumber(ka[1], kb[1]) }); } else if (kb[0] != 1 || kb[1] != 1) { s.push(s.pop() + "scale(" + kb + ")"); } n = q.length; return function(t) { var i = -1, o; while (++i < n) s[(o = q[i]).i] = o.x(t); return s.join(""); }; } function d3_uninterpolateNumber(a, b) { b = (b -= a = +a) || 1 / b; return function(x) { return (x - a) / b; }; } function d3_uninterpolateClamp(a, b) { b = (b -= a = +a) || 1 / b; return function(x) { return Math.max(0, Math.min(1, (x - a) / b)); }; } d3.layout = {}; d3.layout.bundle = function() { return function(links) { var paths = [], i = -1, n = links.length; while (++i < n) paths.push(d3_layout_bundlePath(links[i])); return paths; }; }; function d3_layout_bundlePath(link) { var start = link.source, end = link.target, lca = d3_layout_bundleLeastCommonAncestor(start, end), points = [ start ]; while (start !== lca) { start = start.parent; points.push(start); } var k = points.length; while (end !== lca) { points.splice(k, 0, end); end = end.parent; } return points; } function d3_layout_bundleAncestors(node) { var ancestors = [], parent = node.parent; while (parent != null) { ancestors.push(node); node = parent; parent = parent.parent; } ancestors.push(node); return ancestors; } function d3_layout_bundleLeastCommonAncestor(a, b) { if (a === b) return a; var aNodes = d3_layout_bundleAncestors(a), bNodes = d3_layout_bundleAncestors(b), aNode = aNodes.pop(), bNode = bNodes.pop(), sharedNode = null; while (aNode === bNode) { sharedNode = aNode; aNode = aNodes.pop(); bNode = bNodes.pop(); } return sharedNode; } d3.layout.chord = function() { var chord = {}, chords, groups, matrix, n, padding = 0, sortGroups, sortSubgroups, sortChords; function relayout() { var subgroups = {}, groupSums = [], groupIndex = d3.range(n), subgroupIndex = [], k, x, x0, i, j; chords = []; groups = []; k = 0, i = -1; while (++i < n) { x = 0, j = -1; while (++j < n) { x += matrix[i][j]; } groupSums.push(x); subgroupIndex.push(d3.range(n)); k += x; } if (sortGroups) { groupIndex.sort(function(a, b) { return sortGroups(groupSums[a], groupSums[b]); }); } if (sortSubgroups) { subgroupIndex.forEach(function(d, i) { d.sort(function(a, b) { return sortSubgroups(matrix[i][a], matrix[i][b]); }); }); } k = (τ - padding * n) / k; x = 0, i = -1; while (++i < n) { x0 = x, j = -1; while (++j < n) { var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k; subgroups[di + "-" + dj] = { index: di, subindex: dj, startAngle: a0, endAngle: a1, value: v }; } groups[di] = { index: di, startAngle: x0, endAngle: x, value: (x - x0) / k }; x += padding; } i = -1; while (++i < n) { j = i - 1; while (++j < n) { var source = subgroups[i + "-" + j], target = subgroups[j + "-" + i]; if (source.value || target.value) { chords.push(source.value < target.value ? { source: target, target: source } : { source: source, target: target }); } } } if (sortChords) resort(); } function resort() { chords.sort(function(a, b) { return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2); }); } chord.matrix = function(x) { if (!arguments.length) return matrix; n = (matrix = x) && matrix.length; chords = groups = null; return chord; }; chord.padding = function(x) { if (!arguments.length) return padding; padding = x; chords = groups = null; return chord; }; chord.sortGroups = function(x) { if (!arguments.length) return sortGroups; sortGroups = x; chords = groups = null; return chord; }; chord.sortSubgroups = function(x) { if (!arguments.length) return sortSubgroups; sortSubgroups = x; chords = null; return chord; }; chord.sortChords = function(x) { if (!arguments.length) return sortChords; sortChords = x; if (chords) resort(); return chord; }; chord.chords = function() { if (!chords) relayout(); return chords; }; chord.groups = function() { if (!groups) relayout(); return groups; }; return chord; }; d3.layout.force = function() { var force = {}, event = d3.dispatch("start", "tick", "end"), size = [ 1, 1 ], drag, alpha, friction = .9, linkDistance = d3_layout_forceLinkDistance, linkStrength = d3_layout_forceLinkStrength, charge = -30, chargeDistance2 = d3_layout_forceChargeDistance2, gravity = .1, theta2 = .64, nodes = [], links = [], distances, strengths, charges; function repulse(node) { return function(quad, x1, _, x2) { if (quad.point !== node) { var dx = quad.cx - node.x, dy = quad.cy - node.y, dw = x2 - x1, dn = dx * dx + dy * dy; if (dw * dw / theta2 < dn) { if (dn < chargeDistance2) { var k = quad.charge / dn; node.px -= dx * k; node.py -= dy * k; } return true; } if (quad.point && dn && dn < chargeDistance2) { var k = quad.pointCharge / dn; node.px -= dx * k; node.py -= dy * k; } } return !quad.charge; }; } force.tick = function() { if ((alpha *= .99) < .005) { event.end({ type: "end", alpha: alpha = 0 }); return true; } var n = nodes.length, m = links.length, q, i, o, s, t, l, k, x, y; for (i = 0; i < m; ++i) { o = links[i]; s = o.source; t = o.target; x = t.x - s.x; y = t.y - s.y; if (l = x * x + y * y) { l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l; x *= l; y *= l; t.x -= x * (k = s.weight / (t.weight + s.weight)); t.y -= y * k; s.x += x * (k = 1 - k); s.y += y * k; } } if (k = alpha * gravity) { x = size[0] / 2; y = size[1] / 2; i = -1; if (k) while (++i < n) { o = nodes[i]; o.x += (x - o.x) * k; o.y += (y - o.y) * k; } } if (charge) { d3_layout_forceAccumulate(q = d3.geom.quadtree(nodes), alpha, charges); i = -1; while (++i < n) { if (!(o = nodes[i]).fixed) { q.visit(repulse(o)); } } } i = -1; while (++i < n) { o = nodes[i]; if (o.fixed) { o.x = o.px; o.y = o.py; } else { o.x -= (o.px - (o.px = o.x)) * friction; o.y -= (o.py - (o.py = o.y)) * friction; } } event.tick({ type: "tick", alpha: alpha }); }; force.nodes = function(x) { if (!arguments.length) return nodes; nodes = x; return force; }; force.links = function(x) { if (!arguments.length) return links; links = x; return force; }; force.size = function(x) { if (!arguments.length) return size; size = x; return force; }; force.linkDistance = function(x) { if (!arguments.length) return linkDistance; linkDistance = typeof x === "function" ? x : +x; return force; }; force.distance = force.linkDistance; force.linkStrength = function(x) { if (!arguments.length) return linkStrength; linkStrength = typeof x === "function" ? x : +x; return force; }; force.friction = function(x) { if (!arguments.length) return friction; friction = +x; return force; }; force.charge = function(x) { if (!arguments.length) return charge; charge = typeof x === "function" ? x : +x; return force; }; force.chargeDistance = function(x) { if (!arguments.length) return Math.sqrt(chargeDistance2); chargeDistance2 = x * x; return force; }; force.gravity = function(x) { if (!arguments.length) return gravity; gravity = +x; return force; }; force.theta = function(x) { if (!arguments.length) return Math.sqrt(theta2); theta2 = x * x; return force; }; force.alpha = function(x) { if (!arguments.length) return alpha; x = +x; if (alpha) { if (x > 0) alpha = x; else alpha = 0; } else if (x > 0) { event.start({ type: "start", alpha: alpha = x }); d3.timer(force.tick); } return force; }; force.start = function() { var i, n = nodes.length, m = links.length, w = size[0], h = size[1], neighbors, o; for (i = 0; i < n; ++i) { (o = nodes[i]).index = i; o.weight = 0; } for (i = 0; i < m; ++i) { o = links[i]; if (typeof o.source == "number") o.source = nodes[o.source]; if (typeof o.target == "number") o.target = nodes[o.target]; ++o.source.weight; ++o.target.weight; } for (i = 0; i < n; ++i) { o = nodes[i]; if (isNaN(o.x)) o.x = position("x", w); if (isNaN(o.y)) o.y = position("y", h); if (isNaN(o.px)) o.px = o.x; if (isNaN(o.py)) o.py = o.y; } distances = []; if (typeof linkDistance === "function") for (i = 0; i < m; ++i) distances[i] = +linkDistance.call(this, links[i], i); else for (i = 0; i < m; ++i) distances[i] = linkDistance; strengths = []; if (typeof linkStrength === "function") for (i = 0; i < m; ++i) strengths[i] = +linkStrength.call(this, links[i], i); else for (i = 0; i < m; ++i) strengths[i] = linkStrength; charges = []; if (typeof charge === "function") for (i = 0; i < n; ++i) charges[i] = +charge.call(this, nodes[i], i); else for (i = 0; i < n; ++i) charges[i] = charge; function position(dimension, size) { if (!neighbors) { neighbors = new Array(n); for (j = 0; j < n; ++j) { neighbors[j] = []; } for (j = 0; j < m; ++j) { var o = links[j]; neighbors[o.source.index].push(o.target); neighbors[o.target.index].push(o.source); } } var candidates = neighbors[i], j = -1, m = candidates.length, x; while (++j < m) if (!isNaN(x = candidates[j][dimension])) return x; return Math.random() * size; } return force.resume(); }; force.resume = function() { return force.alpha(.1); }; force.stop = function() { return force.alpha(0); }; force.drag = function() { if (!drag) drag = d3.behavior.drag().origin(d3_identity).on("dragstart.force", d3_layout_forceDragstart).on("drag.force", dragmove).on("dragend.force", d3_layout_forceDragend); if (!arguments.length) return drag; this.on("mouseover.force", d3_layout_forceMouseover).on("mouseout.force", d3_layout_forceMouseout).call(drag); }; function dragmove(d) { d.px = d3.event.x, d.py = d3.event.y; force.resume(); } return d3.rebind(force, event, "on"); }; function d3_layout_forceDragstart(d) { d.fixed |= 2; } function d3_layout_forceDragend(d) { d.fixed &= ~6; } function d3_layout_forceMouseover(d) { d.fixed |= 4; d.px = d.x, d.py = d.y; } function d3_layout_forceMouseout(d) { d.fixed &= ~4; } function d3_layout_forceAccumulate(quad, alpha, charges) { var cx = 0, cy = 0; quad.charge = 0; if (!quad.leaf) { var nodes = quad.nodes, n = nodes.length, i = -1, c; while (++i < n) { c = nodes[i]; if (c == null) continue; d3_layout_forceAccumulate(c, alpha, charges); quad.charge += c.charge; cx += c.charge * c.cx; cy += c.charge * c.cy; } } if (quad.point) { if (!quad.leaf) { quad.point.x += Math.random() - .5; quad.point.y += Math.random() - .5; } var k = alpha * charges[quad.point.index]; quad.charge += quad.pointCharge = k; cx += k * quad.point.x; cy += k * quad.point.y; } quad.cx = cx / quad.charge; quad.cy = cy / quad.charge; } var d3_layout_forceLinkDistance = 20, d3_layout_forceLinkStrength = 1, d3_layout_forceChargeDistance2 = Infinity; d3.layout.hierarchy = function() { var sort = d3_layout_hierarchySort, children = d3_layout_hierarchyChildren, value = d3_layout_hierarchyValue; function hierarchy(root) { var stack = [ root ], nodes = [], node; root.depth = 0; while ((node = stack.pop()) != null) { nodes.push(node); if ((childs = children.call(hierarchy, node, node.depth)) && (n = childs.length)) { var n, childs, child; while (--n >= 0) { stack.push(child = childs[n]); child.parent = node; child.depth = node.depth + 1; } if (value) node.value = 0; node.children = childs; } else { if (value) node.value = +value.call(hierarchy, node, node.depth) || 0; delete node.children; } } d3_layout_hierarchyVisitAfter(root, function(node) { var childs, parent; if (sort && (childs = node.children)) childs.sort(sort); if (value && (parent = node.parent)) parent.value += node.value; }); return nodes; } hierarchy.sort = function(x) { if (!arguments.length) return sort; sort = x; return hierarchy; }; hierarchy.children = function(x) { if (!arguments.length) return children; children = x; return hierarchy; }; hierarchy.value = function(x) { if (!arguments.length) return value; value = x; return hierarchy; }; hierarchy.revalue = function(root) { if (value) { d3_layout_hierarchyVisitBefore(root, function(node) { if (node.children) node.value = 0; }); d3_layout_hierarchyVisitAfter(root, function(node) { var parent; if (!node.children) node.value = +value.call(hierarchy, node, node.depth) || 0; if (parent = node.parent) parent.value += node.value; }); } return root; }; return hierarchy; }; function d3_layout_hierarchyRebind(object, hierarchy) { d3.rebind(object, hierarchy, "sort", "children", "value"); object.nodes = object; object.links = d3_layout_hierarchyLinks; return object; } function d3_layout_hierarchyVisitBefore(node, callback) { var nodes = [ node ]; while ((node = nodes.pop()) != null) { callback(node); if ((children = node.children) && (n = children.length)) { var n, children; while (--n >= 0) nodes.push(children[n]); } } } function d3_layout_hierarchyVisitAfter(node, callback) { var nodes = [ node ], nodes2 = []; while ((node = nodes.pop()) != null) { nodes2.push(node); if ((children = node.children) && (n = children.length)) { var i = -1, n, children; while (++i < n) nodes.push(children[i]); } } while ((node = nodes2.pop()) != null) { callback(node); } } function d3_layout_hierarchyChildren(d) { return d.children; } function d3_layout_hierarchyValue(d) { return d.value; } function d3_layout_hierarchySort(a, b) { return b.value - a.value; } function d3_layout_hierarchyLinks(nodes) { return d3.merge(nodes.map(function(parent) { return (parent.children || []).map(function(child) { return { source: parent, target: child }; }); })); } d3.layout.partition = function() { var hierarchy = d3.layout.hierarchy(), size = [ 1, 1 ]; function position(node, x, dx, dy) { var children = node.children; node.x = x; node.y = node.depth * dy; node.dx = dx; node.dy = dy; if (children && (n = children.length)) { var i = -1, n, c, d; dx = node.value ? dx / node.value : 0; while (++i < n) { position(c = children[i], x, d = c.value * dx, dy); x += d; } } } function depth(node) { var children = node.children, d = 0; if (children && (n = children.length)) { var i = -1, n; while (++i < n) d = Math.max(d, depth(children[i])); } return 1 + d; } function partition(d, i) { var nodes = hierarchy.call(this, d, i); position(nodes[0], 0, size[0], size[1] / depth(nodes[0])); return nodes; } partition.size = function(x) { if (!arguments.length) return size; size = x; return partition; }; return d3_layout_hierarchyRebind(partition, hierarchy); }; d3.layout.pie = function() { var value = Number, sort = d3_layout_pieSortByValue, startAngle = 0, endAngle = τ, padAngle = 0; function pie(data) { var n = data.length, values = data.map(function(d, i) { return +value.call(pie, d, i); }), a = +(typeof startAngle === "function" ? startAngle.apply(this, arguments) : startAngle), da = (typeof endAngle === "function" ? endAngle.apply(this, arguments) : endAngle) - a, p = Math.min(Math.abs(da) / n, +(typeof padAngle === "function" ? padAngle.apply(this, arguments) : padAngle)), pa = p * (da < 0 ? -1 : 1), k = (da - n * pa) / d3.sum(values), index = d3.range(n), arcs = [], v; if (sort != null) index.sort(sort === d3_layout_pieSortByValue ? function(i, j) { return values[j] - values[i]; } : function(i, j) { return sort(data[i], data[j]); }); index.forEach(function(i) { arcs[i] = { data: data[i], value: v = values[i], startAngle: a, endAngle: a += v * k + pa, padAngle: p }; }); return arcs; } pie.value = function(_) { if (!arguments.length) return value; value = _; return pie; }; pie.sort = function(_) { if (!arguments.length) return sort; sort = _; return pie; }; pie.startAngle = function(_) { if (!arguments.length) return startAngle; startAngle = _; return pie; }; pie.endAngle = function(_) { if (!arguments.length) return endAngle; endAngle = _; return pie; }; pie.padAngle = function(_) { if (!arguments.length) return padAngle; padAngle = _; return pie; }; return pie; }; var d3_layout_pieSortByValue = {}; d3.layout.stack = function() { var values = d3_identity, order = d3_layout_stackOrderDefault, offset = d3_layout_stackOffsetZero, out = d3_layout_stackOut, x = d3_layout_stackX, y = d3_layout_stackY; function stack(data, index) { if (!(n = data.length)) return data; var series = data.map(function(d, i) { return values.call(stack, d, i); }); var points = series.map(function(d) { return d.map(function(v, i) { return [ x.call(stack, v, i), y.call(stack, v, i) ]; }); }); var orders = order.call(stack, points, index); series = d3.permute(series, orders); points = d3.permute(points, orders); var offsets = offset.call(stack, points, index); var m = series[0].length, n, i, j, o; for (j = 0; j < m; ++j) { out.call(stack, series[0][j], o = offsets[j], points[0][j][1]); for (i = 1; i < n; ++i) { out.call(stack, series[i][j], o += points[i - 1][j][1], points[i][j][1]); } } return data; } stack.values = function(x) { if (!arguments.length) return values; values = x; return stack; }; stack.order = function(x) { if (!arguments.length) return order; order = typeof x === "function" ? x : d3_layout_stackOrders.get(x) || d3_layout_stackOrderDefault; return stack; }; stack.offset = function(x) { if (!arguments.length) return offset; offset = typeof x === "function" ? x : d3_layout_stackOffsets.get(x) || d3_layout_stackOffsetZero; return stack; }; stack.x = function(z) { if (!arguments.length) return x; x = z; return stack; }; stack.y = function(z) { if (!arguments.length) return y; y = z; return stack; }; stack.out = function(z) { if (!arguments.length) return out; out = z; return stack; }; return stack; }; function d3_layout_stackX(d) { return d.x; } function d3_layout_stackY(d) { return d.y; } function d3_layout_stackOut(d, y0, y) { d.y0 = y0; d.y = y; } var d3_layout_stackOrders = d3.map({ "inside-out": function(data) { var n = data.length, i, j, max = data.map(d3_layout_stackMaxIndex), sums = data.map(d3_layout_stackReduceSum), index = d3.range(n).sort(function(a, b) { return max[a] - max[b]; }), top = 0, bottom = 0, tops = [], bottoms = []; for (i = 0; i < n; ++i) { j = index[i]; if (top < bottom) { top += sums[j]; tops.push(j); } else { bottom += sums[j]; bottoms.push(j); } } return bottoms.reverse().concat(tops); }, reverse: function(data) { return d3.range(data.length).reverse(); }, "default": d3_layout_stackOrderDefault }); var d3_layout_stackOffsets = d3.map({ silhouette: function(data) { var n = data.length, m = data[0].length, sums = [], max = 0, i, j, o, y0 = []; for (j = 0; j < m; ++j) { for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; if (o > max) max = o; sums.push(o); } for (j = 0; j < m; ++j) { y0[j] = (max - sums[j]) / 2; } return y0; }, wiggle: function(data) { var n = data.length, x = data[0], m = x.length, i, j, k, s1, s2, s3, dx, o, o0, y0 = []; y0[0] = o = o0 = 0; for (j = 1; j < m; ++j) { for (i = 0, s1 = 0; i < n; ++i) s1 += data[i][j][1]; for (i = 0, s2 = 0, dx = x[j][0] - x[j - 1][0]; i < n; ++i) { for (k = 0, s3 = (data[i][j][1] - data[i][j - 1][1]) / (2 * dx); k < i; ++k) { s3 += (data[k][j][1] - data[k][j - 1][1]) / dx; } s2 += s3 * data[i][j][1]; } y0[j] = o -= s1 ? s2 / s1 * dx : 0; if (o < o0) o0 = o; } for (j = 0; j < m; ++j) y0[j] -= o0; return y0; }, expand: function(data) { var n = data.length, m = data[0].length, k = 1 / n, i, j, o, y0 = []; for (j = 0; j < m; ++j) { for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; if (o) for (i = 0; i < n; i++) data[i][j][1] /= o; else for (i = 0; i < n; i++) data[i][j][1] = k; } for (j = 0; j < m; ++j) y0[j] = 0; return y0; }, zero: d3_layout_stackOffsetZero }); function d3_layout_stackOrderDefault(data) { return d3.range(data.length); } function d3_layout_stackOffsetZero(data) { var j = -1, m = data[0].length, y0 = []; while (++j < m) y0[j] = 0; return y0; } function d3_layout_stackMaxIndex(array) { var i = 1, j = 0, v = array[0][1], k, n = array.length; for (;i < n; ++i) { if ((k = array[i][1]) > v) { j = i; v = k; } } return j; } function d3_layout_stackReduceSum(d) { return d.reduce(d3_layout_stackSum, 0); } function d3_layout_stackSum(p, d) { return p + d[1]; } d3.layout.histogram = function() { var frequency = true, valuer = Number, ranger = d3_layout_histogramRange, binner = d3_layout_histogramBinSturges; function histogram(data, i) { var bins = [], values = data.map(valuer, this), range = ranger.call(this, values, i), thresholds = binner.call(this, range, values, i), bin, i = -1, n = values.length, m = thresholds.length - 1, k = frequency ? 1 : 1 / n, x; while (++i < m) { bin = bins[i] = []; bin.dx = thresholds[i + 1] - (bin.x = thresholds[i]); bin.y = 0; } if (m > 0) { i = -1; while (++i < n) { x = values[i]; if (x >= range[0] && x <= range[1]) { bin = bins[d3.bisect(thresholds, x, 1, m) - 1]; bin.y += k; bin.push(data[i]); } } } return bins; } histogram.value = function(x) { if (!arguments.length) return valuer; valuer = x; return histogram; }; histogram.range = function(x) { if (!arguments.length) return ranger; ranger = d3_functor(x); return histogram; }; histogram.bins = function(x) { if (!arguments.length) return binner; binner = typeof x === "number" ? function(range) { return d3_layout_histogramBinFixed(range, x); } : d3_functor(x); return histogram; }; histogram.frequency = function(x) { if (!arguments.length) return frequency; frequency = !!x; return histogram; }; return histogram; }; function d3_layout_histogramBinSturges(range, values) { return d3_layout_histogramBinFixed(range, Math.ceil(Math.log(values.length) / Math.LN2 + 1)); } function d3_layout_histogramBinFixed(range, n) { var x = -1, b = +range[0], m = (range[1] - b) / n, f = []; while (++x <= n) f[x] = m * x + b; return f; } function d3_layout_histogramRange(values) { return [ d3.min(values), d3.max(values) ]; } d3.layout.pack = function() { var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), padding = 0, size = [ 1, 1 ], radius; function pack(d, i) { var nodes = hierarchy.call(this, d, i), root = nodes[0], w = size[0], h = size[1], r = radius == null ? Math.sqrt : typeof radius === "function" ? radius : function() { return radius; }; root.x = root.y = 0; d3_layout_hierarchyVisitAfter(root, function(d) { d.r = +r(d.value); }); d3_layout_hierarchyVisitAfter(root, d3_layout_packSiblings); if (padding) { var dr = padding * (radius ? 1 : Math.max(2 * root.r / w, 2 * root.r / h)) / 2; d3_layout_hierarchyVisitAfter(root, function(d) { d.r += dr; }); d3_layout_hierarchyVisitAfter(root, d3_layout_packSiblings); d3_layout_hierarchyVisitAfter(root, function(d) { d.r -= dr; }); } d3_layout_packTransform(root, w / 2, h / 2, radius ? 1 : 1 / Math.max(2 * root.r / w, 2 * root.r / h)); return nodes; } pack.size = function(_) { if (!arguments.length) return size; size = _; return pack; }; pack.radius = function(_) { if (!arguments.length) return radius; radius = _ == null || typeof _ === "function" ? _ : +_; return pack; }; pack.padding = function(_) { if (!arguments.length) return padding; padding = +_; return pack; }; return d3_layout_hierarchyRebind(pack, hierarchy); }; function d3_layout_packSort(a, b) { return a.value - b.value; } function d3_layout_packInsert(a, b) { var c = a._pack_next; a._pack_next = b; b._pack_prev = a; b._pack_next = c; c._pack_prev = b; } function d3_layout_packSplice(a, b) { a._pack_next = b; b._pack_prev = a; } function d3_layout_packIntersects(a, b) { var dx = b.x - a.x, dy = b.y - a.y, dr = a.r + b.r; return .999 * dr * dr > dx * dx + dy * dy; } function d3_layout_packSiblings(node) { if (!(nodes = node.children) || !(n = nodes.length)) return; var nodes, xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity, a, b, c, i, j, k, n; function bound(node) { xMin = Math.min(node.x - node.r, xMin); xMax = Math.max(node.x + node.r, xMax); yMin = Math.min(node.y - node.r, yMin); yMax = Math.max(node.y + node.r, yMax); } nodes.forEach(d3_layout_packLink); a = nodes[0]; a.x = -a.r; a.y = 0; bound(a); if (n > 1) { b = nodes[1]; b.x = b.r; b.y = 0; bound(b); if (n > 2) { c = nodes[2]; d3_layout_packPlace(a, b, c); bound(c); d3_layout_packInsert(a, c); a._pack_prev = c; d3_layout_packInsert(c, b); b = a._pack_next; for (i = 3; i < n; i++) { d3_layout_packPlace(a, b, c = nodes[i]); var isect = 0, s1 = 1, s2 = 1; for (j = b._pack_next; j !== b; j = j._pack_next, s1++) { if (d3_layout_packIntersects(j, c)) { isect = 1; break; } } if (isect == 1) { for (k = a._pack_prev; k !== j._pack_prev; k = k._pack_prev, s2++) { if (d3_layout_packIntersects(k, c)) { break; } } } if (isect) { if (s1 < s2 || s1 == s2 && b.r < a.r) d3_layout_packSplice(a, b = j); else d3_layout_packSplice(a = k, b); i--; } else { d3_layout_packInsert(a, c); b = c; bound(c); } } } } var cx = (xMin + xMax) / 2, cy = (yMin + yMax) / 2, cr = 0; for (i = 0; i < n; i++) { c = nodes[i]; c.x -= cx; c.y -= cy; cr = Math.max(cr, c.r + Math.sqrt(c.x * c.x + c.y * c.y)); } node.r = cr; nodes.forEach(d3_layout_packUnlink); } function d3_layout_packLink(node) { node._pack_next = node._pack_prev = node; } function d3_layout_packUnlink(node) { delete node._pack_next; delete node._pack_prev; } function d3_layout_packTransform(node, x, y, k) { var children = node.children; node.x = x += k * node.x; node.y = y += k * node.y; node.r *= k; if (children) { var i = -1, n = children.length; while (++i < n) d3_layout_packTransform(children[i], x, y, k); } } function d3_layout_packPlace(a, b, c) { var db = a.r + c.r, dx = b.x - a.x, dy = b.y - a.y; if (db && (dx || dy)) { var da = b.r + c.r, dc = dx * dx + dy * dy; da *= da; db *= db; var x = .5 + (db - da) / (2 * dc), y = Math.sqrt(Math.max(0, 2 * da * (db + dc) - (db -= dc) * db - da * da)) / (2 * dc); c.x = a.x + x * dx + y * dy; c.y = a.y + x * dy - y * dx; } else { c.x = a.x + db; c.y = a.y; } } d3.layout.tree = function() { var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = null; function tree(d, i) { var nodes = hierarchy.call(this, d, i), root0 = nodes[0], root1 = wrapTree(root0); d3_layout_hierarchyVisitAfter(root1, firstWalk), root1.parent.m = -root1.z; d3_layout_hierarchyVisitBefore(root1, secondWalk); if (nodeSize) d3_layout_hierarchyVisitBefore(root0, sizeNode); else { var left = root0, right = root0, bottom = root0; d3_layout_hierarchyVisitBefore(root0, function(node) { if (node.x < left.x) left = node; if (node.x > right.x) right = node; if (node.depth > bottom.depth) bottom = node; }); var tx = separation(left, right) / 2 - left.x, kx = size[0] / (right.x + separation(right, left) / 2 + tx), ky = size[1] / (bottom.depth || 1); d3_layout_hierarchyVisitBefore(root0, function(node) { node.x = (node.x + tx) * kx; node.y = node.depth * ky; }); } return nodes; } function wrapTree(root0) { var root1 = { A: null, children: [ root0 ] }, queue = [ root1 ], node1; while ((node1 = queue.pop()) != null) { for (var children = node1.children, child, i = 0, n = children.length; i < n; ++i) { queue.push((children[i] = child = { _: children[i], parent: node1, children: (child = children[i].children) && child.slice() || [], A: null, a: null, z: 0, m: 0, c: 0, s: 0, t: null, i: i }).a = child); } } return root1.children[0]; } function firstWalk(v) { var children = v.children, siblings = v.parent.children, w = v.i ? siblings[v.i - 1] : null; if (children.length) { d3_layout_treeShift(v); var midpoint = (children[0].z + children[children.length - 1].z) / 2; if (w) { v.z = w.z + separation(v._, w._); v.m = v.z - midpoint; } else { v.z = midpoint; } } else if (w) { v.z = w.z + separation(v._, w._); } v.parent.A = apportion(v, w, v.parent.A || siblings[0]); } function secondWalk(v) { v._.x = v.z + v.parent.m; v.m += v.parent.m; } function apportion(v, w, ancestor) { if (w) { var vip = v, vop = v, vim = w, vom = vip.parent.children[0], sip = vip.m, sop = vop.m, sim = vim.m, som = vom.m, shift; while (vim = d3_layout_treeRight(vim), vip = d3_layout_treeLeft(vip), vim && vip) { vom = d3_layout_treeLeft(vom); vop = d3_layout_treeRight(vop); vop.a = v; shift = vim.z + sim - vip.z - sip + separation(vim._, vip._); if (shift > 0) { d3_layout_treeMove(d3_layout_treeAncestor(vim, v, ancestor), v, shift); sip += shift; sop += shift; } sim += vim.m; sip += vip.m; som += vom.m; sop += vop.m; } if (vim && !d3_layout_treeRight(vop)) { vop.t = vim; vop.m += sim - sop; } if (vip && !d3_layout_treeLeft(vom)) { vom.t = vip; vom.m += sip - som; ancestor = v; } } return ancestor; } function sizeNode(node) { node.x *= size[0]; node.y = node.depth * size[1]; } tree.separation = function(x) { if (!arguments.length) return separation; separation = x; return tree; }; tree.size = function(x) { if (!arguments.length) return nodeSize ? null : size; nodeSize = (size = x) == null ? sizeNode : null; return tree; }; tree.nodeSize = function(x) { if (!arguments.length) return nodeSize ? size : null; nodeSize = (size = x) == null ? null : sizeNode; return tree; }; return d3_layout_hierarchyRebind(tree, hierarchy); }; function d3_layout_treeSeparation(a, b) { return a.parent == b.parent ? 1 : 2; } function d3_layout_treeLeft(v) { var children = v.children; return children.length ? children[0] : v.t; } function d3_layout_treeRight(v) { var children = v.children, n; return (n = children.length) ? children[n - 1] : v.t; } function d3_layout_treeMove(wm, wp, shift) { var change = shift / (wp.i - wm.i); wp.c -= change; wp.s += shift; wm.c += change; wp.z += shift; wp.m += shift; } function d3_layout_treeShift(v) { var shift = 0, change = 0, children = v.children, i = children.length, w; while (--i >= 0) { w = children[i]; w.z += shift; w.m += shift; shift += w.s + (change += w.c); } } function d3_layout_treeAncestor(vim, v, ancestor) { return vim.a.parent === v.parent ? vim.a : ancestor; } d3.layout.cluster = function() { var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = false; function cluster(d, i) { var nodes = hierarchy.call(this, d, i), root = nodes[0], previousNode, x = 0; d3_layout_hierarchyVisitAfter(root, function(node) { var children = node.children; if (children && children.length) { node.x = d3_layout_clusterX(children); node.y = d3_layout_clusterY(children); } else { node.x = previousNode ? x += separation(node, previousNode) : 0; node.y = 0; previousNode = node; } }); var left = d3_layout_clusterLeft(root), right = d3_layout_clusterRight(root), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2; d3_layout_hierarchyVisitAfter(root, nodeSize ? function(node) { node.x = (node.x - root.x) * size[0]; node.y = (root.y - node.y) * size[1]; } : function(node) { node.x = (node.x - x0) / (x1 - x0) * size[0]; node.y = (1 - (root.y ? node.y / root.y : 1)) * size[1]; }); return nodes; } cluster.separation = function(x) { if (!arguments.length) return separation; separation = x; return cluster; }; cluster.size = function(x) { if (!arguments.length) return nodeSize ? null : size; nodeSize = (size = x) == null; return cluster; }; cluster.nodeSize = function(x) { if (!arguments.length) return nodeSize ? size : null; nodeSize = (size = x) != null; return cluster; }; return d3_layout_hierarchyRebind(cluster, hierarchy); }; function d3_layout_clusterY(children) { return 1 + d3.max(children, function(child) { return child.y; }); } function d3_layout_clusterX(children) { return children.reduce(function(x, child) { return x + child.x; }, 0) / children.length; } function d3_layout_clusterLeft(node) { var children = node.children; return children && children.length ? d3_layout_clusterLeft(children[0]) : node; } function d3_layout_clusterRight(node) { var children = node.children, n; return children && (n = children.length) ? d3_layout_clusterRight(children[n - 1]) : node; } d3.layout.treemap = function() { var hierarchy = d3.layout.hierarchy(), round = Math.round, size = [ 1, 1 ], padding = null, pad = d3_layout_treemapPadNull, sticky = false, stickies, mode = "squarify", ratio = .5 * (1 + Math.sqrt(5)); function scale(children, k) { var i = -1, n = children.length, child, area; while (++i < n) { area = (child = children[i]).value * (k < 0 ? 0 : k); child.area = isNaN(area) || area <= 0 ? 0 : area; } } function squarify(node) { var children = node.children; if (children && children.length) { var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === "slice" ? rect.dx : mode === "dice" ? rect.dy : mode === "slice-dice" ? node.depth & 1 ? rect.dy : rect.dx : Math.min(rect.dx, rect.dy), n; scale(remaining, rect.dx * rect.dy / node.value); row.area = 0; while ((n = remaining.length) > 0) { row.push(child = remaining[n - 1]); row.area += child.area; if (mode !== "squarify" || (score = worst(row, u)) <= best) { remaining.pop(); best = score; } else { row.area -= row.pop().area; position(row, u, rect, false); u = Math.min(rect.dx, rect.dy); row.length = row.area = 0; best = Infinity; } } if (row.length) { position(row, u, rect, true); row.length = row.area = 0; } children.forEach(squarify); } } function stickify(node) { var children = node.children; if (children && children.length) { var rect = pad(node), remaining = children.slice(), child, row = []; scale(remaining, rect.dx * rect.dy / node.value); row.area = 0; while (child = remaining.pop()) { row.push(child); row.area += child.area; if (child.z != null) { position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length); row.length = row.area = 0; } } children.forEach(stickify); } } function worst(row, u) { var s = row.area, r, rmax = 0, rmin = Infinity, i = -1, n = row.length; while (++i < n) { if (!(r = row[i].area)) continue; if (r < rmin) rmin = r; if (r > rmax) rmax = r; } s *= s; u *= u; return s ? Math.max(u * rmax * ratio / s, s / (u * rmin * ratio)) : Infinity; } function position(row, u, rect, flush) { var i = -1, n = row.length, x = rect.x, y = rect.y, v = u ? round(row.area / u) : 0, o; if (u == rect.dx) { if (flush || v > rect.dy) v = rect.dy; while (++i < n) { o = row[i]; o.x = x; o.y = y; o.dy = v; x += o.dx = Math.min(rect.x + rect.dx - x, v ? round(o.area / v) : 0); } o.z = true; o.dx += rect.x + rect.dx - x; rect.y += v; rect.dy -= v; } else { if (flush || v > rect.dx) v = rect.dx; while (++i < n) { o = row[i]; o.x = x; o.y = y; o.dx = v; y += o.dy = Math.min(rect.y + rect.dy - y, v ? round(o.area / v) : 0); } o.z = false; o.dy += rect.y + rect.dy - y; rect.x += v; rect.dx -= v; } } function treemap(d) { var nodes = stickies || hierarchy(d), root = nodes[0]; root.x = 0; root.y = 0; root.dx = size[0]; root.dy = size[1]; if (stickies) hierarchy.revalue(root); scale([ root ], root.dx * root.dy / root.value); (stickies ? stickify : squarify)(root); if (sticky) stickies = nodes; return nodes; } treemap.size = function(x) { if (!arguments.length) return size; size = x; return treemap; }; treemap.padding = function(x) { if (!arguments.length) return padding; function padFunction(node) { var p = x.call(treemap, node, node.depth); return p == null ? d3_layout_treemapPadNull(node) : d3_layout_treemapPad(node, typeof p === "number" ? [ p, p, p, p ] : p); } function padConstant(node) { return d3_layout_treemapPad(node, x); } var type; pad = (padding = x) == null ? d3_layout_treemapPadNull : (type = typeof x) === "function" ? padFunction : type === "number" ? (x = [ x, x, x, x ], padConstant) : padConstant; return treemap; }; treemap.round = function(x) { if (!arguments.length) return round != Number; round = x ? Math.round : Number; return treemap; }; treemap.sticky = function(x) { if (!arguments.length) return sticky; sticky = x; stickies = null; return treemap; }; treemap.ratio = function(x) { if (!arguments.length) return ratio; ratio = x; return treemap; }; treemap.mode = function(x) { if (!arguments.length) return mode; mode = x + ""; return treemap; }; return d3_layout_hierarchyRebind(treemap, hierarchy); }; function d3_layout_treemapPadNull(node) { return { x: node.x, y: node.y, dx: node.dx, dy: node.dy }; } function d3_layout_treemapPad(node, padding) { var x = node.x + padding[3], y = node.y + padding[0], dx = node.dx - padding[1] - padding[3], dy = node.dy - padding[0] - padding[2]; if (dx < 0) { x += dx / 2; dx = 0; } if (dy < 0) { y += dy / 2; dy = 0; } return { x: x, y: y, dx: dx, dy: dy }; } d3.random = { normal: function(µ, σ) { var n = arguments.length; if (n < 2) σ = 1; if (n < 1) µ = 0; return function() { var x, y, r; do { x = Math.random() * 2 - 1; y = Math.random() * 2 - 1; r = x * x + y * y; } while (!r || r > 1); return µ + σ * x * Math.sqrt(-2 * Math.log(r) / r); }; }, logNormal: function() { var random = d3.random.normal.apply(d3, arguments); return function() { return Math.exp(random()); }; }, bates: function(m) { var random = d3.random.irwinHall(m); return function() { return random() / m; }; }, irwinHall: function(m) { return function() { for (var s = 0, j = 0; j < m; j++) s += Math.random(); return s; }; } }; d3.scale = {}; function d3_scaleExtent(domain) { var start = domain[0], stop = domain[domain.length - 1]; return start < stop ? [ start, stop ] : [ stop, start ]; } function d3_scaleRange(scale) { return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range()); } function d3_scale_bilinear(domain, range, uninterpolate, interpolate) { var u = uninterpolate(domain[0], domain[1]), i = interpolate(range[0], range[1]); return function(x) { return i(u(x)); }; } function d3_scale_nice(domain, nice) { var i0 = 0, i1 = domain.length - 1, x0 = domain[i0], x1 = domain[i1], dx; if (x1 < x0) { dx = i0, i0 = i1, i1 = dx; dx = x0, x0 = x1, x1 = dx; } domain[i0] = nice.floor(x0); domain[i1] = nice.ceil(x1); return domain; } function d3_scale_niceStep(step) { return step ? { floor: function(x) { return Math.floor(x / step) * step; }, ceil: function(x) { return Math.ceil(x / step) * step; } } : d3_scale_niceIdentity; } var d3_scale_niceIdentity = { floor: d3_identity, ceil: d3_identity }; function d3_scale_polylinear(domain, range, uninterpolate, interpolate) { var u = [], i = [], j = 0, k = Math.min(domain.length, range.length) - 1; if (domain[k] < domain[0]) { domain = domain.slice().reverse(); range = range.slice().reverse(); } while (++j <= k) { u.push(uninterpolate(domain[j - 1], domain[j])); i.push(interpolate(range[j - 1], range[j])); } return function(x) { var j = d3.bisect(domain, x, 1, k) - 1; return i[j](u[j](x)); }; } d3.scale.linear = function() { return d3_scale_linear([ 0, 1 ], [ 0, 1 ], d3_interpolate, false); }; function d3_scale_linear(domain, range, interpolate, clamp) { var output, input; function rescale() { var linear = Math.min(domain.length, range.length) > 2 ? d3_scale_polylinear : d3_scale_bilinear, uninterpolate = clamp ? d3_uninterpolateClamp : d3_uninterpolateNumber; output = linear(domain, range, uninterpolate, interpolate); input = linear(range, domain, uninterpolate, d3_interpolate); return scale; } function scale(x) { return output(x); } scale.invert = function(y) { return input(y); }; scale.domain = function(x) { if (!arguments.length) return domain; domain = x.map(Number); return rescale(); }; scale.range = function(x) { if (!arguments.length) return range; range = x; return rescale(); }; scale.rangeRound = function(x) { return scale.range(x).interpolate(d3_interpolateRound); }; scale.clamp = function(x) { if (!arguments.length) return clamp; clamp = x; return rescale(); }; scale.interpolate = function(x) { if (!arguments.length) return interpolate; interpolate = x; return rescale(); }; scale.ticks = function(m) { return d3_scale_linearTicks(domain, m); }; scale.tickFormat = function(m, format) { return d3_scale_linearTickFormat(domain, m, format); }; scale.nice = function(m) { d3_scale_linearNice(domain, m); return rescale(); }; scale.copy = function() { return d3_scale_linear(domain, range, interpolate, clamp); }; return rescale(); } function d3_scale_linearRebind(scale, linear) { return d3.rebind(scale, linear, "range", "rangeRound", "interpolate", "clamp"); } function d3_scale_linearNice(domain, m) { return d3_scale_nice(domain, d3_scale_niceStep(d3_scale_linearTickRange(domain, m)[2])); } function d3_scale_linearTickRange(domain, m) { if (m == null) m = 10; var extent = d3_scaleExtent(domain), span = extent[1] - extent[0], step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)), err = m / span * step; if (err <= .15) step *= 10; else if (err <= .35) step *= 5; else if (err <= .75) step *= 2; extent[0] = Math.ceil(extent[0] / step) * step; extent[1] = Math.floor(extent[1] / step) * step + step * .5; extent[2] = step; return extent; } function d3_scale_linearTicks(domain, m) { return d3.range.apply(d3, d3_scale_linearTickRange(domain, m)); } function d3_scale_linearTickFormat(domain, m, format) { var range = d3_scale_linearTickRange(domain, m); if (format) { var match = d3_format_re.exec(format); match.shift(); if (match[8] === "s") { var prefix = d3.formatPrefix(Math.max(abs(range[0]), abs(range[1]))); if (!match[7]) match[7] = "." + d3_scale_linearPrecision(prefix.scale(range[2])); match[8] = "f"; format = d3.format(match.join("")); return function(d) { return format(prefix.scale(d)) + prefix.symbol; }; } if (!match[7]) match[7] = "." + d3_scale_linearFormatPrecision(match[8], range); format = match.join(""); } else { format = ",." + d3_scale_linearPrecision(range[2]) + "f"; } return d3.format(format); } var d3_scale_linearFormatSignificant = { s: 1, g: 1, p: 1, r: 1, e: 1 }; function d3_scale_linearPrecision(value) { return -Math.floor(Math.log(value) / Math.LN10 + .01); } function d3_scale_linearFormatPrecision(type, range) { var p = d3_scale_linearPrecision(range[2]); return type in d3_scale_linearFormatSignificant ? Math.abs(p - d3_scale_linearPrecision(Math.max(abs(range[0]), abs(range[1])))) + +(type !== "e") : p - (type === "%") * 2; } d3.scale.log = function() { return d3_scale_log(d3.scale.linear().domain([ 0, 1 ]), 10, true, [ 1, 10 ]); }; function d3_scale_log(linear, base, positive, domain) { function log(x) { return (positive ? Math.log(x < 0 ? 0 : x) : -Math.log(x > 0 ? 0 : -x)) / Math.log(base); } function pow(x) { return positive ? Math.pow(base, x) : -Math.pow(base, -x); } function scale(x) { return linear(log(x)); } scale.invert = function(x) { return pow(linear.invert(x)); }; scale.domain = function(x) { if (!arguments.length) return domain; positive = x[0] >= 0; linear.domain((domain = x.map(Number)).map(log)); return scale; }; scale.base = function(_) { if (!arguments.length) return base; base = +_; linear.domain(domain.map(log)); return scale; }; scale.nice = function() { var niced = d3_scale_nice(domain.map(log), positive ? Math : d3_scale_logNiceNegative); linear.domain(niced); domain = niced.map(pow); return scale; }; scale.ticks = function() { var extent = d3_scaleExtent(domain), ticks = [], u = extent[0], v = extent[1], i = Math.floor(log(u)), j = Math.ceil(log(v)), n = base % 1 ? 2 : base; if (isFinite(j - i)) { if (positive) { for (;i < j; i++) for (var k = 1; k < n; k++) ticks.push(pow(i) * k); ticks.push(pow(i)); } else { ticks.push(pow(i)); for (;i++ < j; ) for (var k = n - 1; k > 0; k--) ticks.push(pow(i) * k); } for (i = 0; ticks[i] < u; i++) {} for (j = ticks.length; ticks[j - 1] > v; j--) {} ticks = ticks.slice(i, j); } return ticks; }; scale.tickFormat = function(n, format) { if (!arguments.length) return d3_scale_logFormat; if (arguments.length < 2) format = d3_scale_logFormat; else if (typeof format !== "function") format = d3.format(format); var k = Math.max(.1, n / scale.ticks().length), f = positive ? (e = 1e-12, Math.ceil) : (e = -1e-12, Math.floor), e; return function(d) { return d / pow(f(log(d) + e)) <= k ? format(d) : ""; }; }; scale.copy = function() { return d3_scale_log(linear.copy(), base, positive, domain); }; return d3_scale_linearRebind(scale, linear); } var d3_scale_logFormat = d3.format(".0e"), d3_scale_logNiceNegative = { floor: function(x) { return -Math.ceil(-x); }, ceil: function(x) { return -Math.floor(-x); } }; d3.scale.pow = function() { return d3_scale_pow(d3.scale.linear(), 1, [ 0, 1 ]); }; function d3_scale_pow(linear, exponent, domain) { var powp = d3_scale_powPow(exponent), powb = d3_scale_powPow(1 / exponent); function scale(x) { return linear(powp(x)); } scale.invert = function(x) { return powb(linear.invert(x)); }; scale.domain = function(x) { if (!arguments.length) return domain; linear.domain((domain = x.map(Number)).map(powp)); return scale; }; scale.ticks = function(m) { return d3_scale_linearTicks(domain, m); }; scale.tickFormat = function(m, format) { return d3_scale_linearTickFormat(domain, m, format); }; scale.nice = function(m) { return scale.domain(d3_scale_linearNice(domain, m)); }; scale.exponent = function(x) { if (!arguments.length) return exponent; powp = d3_scale_powPow(exponent = x); powb = d3_scale_powPow(1 / exponent); linear.domain(domain.map(powp)); return scale; }; scale.copy = function() { return d3_scale_pow(linear.copy(), exponent, domain); }; return d3_scale_linearRebind(scale, linear); } function d3_scale_powPow(e) { return function(x) { return x < 0 ? -Math.pow(-x, e) : Math.pow(x, e); }; } d3.scale.sqrt = function() { return d3.scale.pow().exponent(.5); }; d3.scale.ordinal = function() { return d3_scale_ordinal([], { t: "range", a: [ [] ] }); }; function d3_scale_ordinal(domain, ranger) { var index, range, rangeBand; function scale(x) { return range[((index.get(x) || (ranger.t === "range" ? index.set(x, domain.push(x)) : NaN)) - 1) % range.length]; } function steps(start, step) { return d3.range(domain.length).map(function(i) { return start + step * i; }); } scale.domain = function(x) { if (!arguments.length) return domain; domain = []; index = new d3_Map(); var i = -1, n = x.length, xi; while (++i < n) if (!index.has(xi = x[i])) index.set(xi, domain.push(xi)); return scale[ranger.t].apply(scale, ranger.a); }; scale.range = function(x) { if (!arguments.length) return range; range = x; rangeBand = 0; ranger = { t: "range", a: arguments }; return scale; }; scale.rangePoints = function(x, padding) { if (arguments.length < 2) padding = 0; var start = x[0], stop = x[1], step = domain.length < 2 ? (start = (start + stop) / 2, 0) : (stop - start) / (domain.length - 1 + padding); range = steps(start + step * padding / 2, step); rangeBand = 0; ranger = { t: "rangePoints", a: arguments }; return scale; }; scale.rangeRoundPoints = function(x, padding) { if (arguments.length < 2) padding = 0; var start = x[0], stop = x[1], step = domain.length < 2 ? (start = stop = Math.round((start + stop) / 2), 0) : (stop - start) / (domain.length - 1 + padding) | 0; range = steps(start + Math.round(step * padding / 2 + (stop - start - (domain.length - 1 + padding) * step) / 2), step); rangeBand = 0; ranger = { t: "rangeRoundPoints", a: arguments }; return scale; }; scale.rangeBands = function(x, padding, outerPadding) { if (arguments.length < 2) padding = 0; if (arguments.length < 3) outerPadding = padding; var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = (stop - start) / (domain.length - padding + 2 * outerPadding); range = steps(start + step * outerPadding, step); if (reverse) range.reverse(); rangeBand = step * (1 - padding); ranger = { t: "rangeBands", a: arguments }; return scale; }; scale.rangeRoundBands = function(x, padding, outerPadding) { if (arguments.length < 2) padding = 0; if (arguments.length < 3) outerPadding = padding; var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = Math.floor((stop - start) / (domain.length - padding + 2 * outerPadding)); range = steps(start + Math.round((stop - start - (domain.length - padding) * step) / 2), step); if (reverse) range.reverse(); rangeBand = Math.round(step * (1 - padding)); ranger = { t: "rangeRoundBands", a: arguments }; return scale; }; scale.rangeBand = function() { return rangeBand; }; scale.rangeExtent = function() { return d3_scaleExtent(ranger.a[0]); }; scale.copy = function() { return d3_scale_ordinal(domain, ranger); }; return scale.domain(domain); } d3.scale.category10 = function() { return d3.scale.ordinal().range(d3_category10); }; d3.scale.category20 = function() { return d3.scale.ordinal().range(d3_category20); }; d3.scale.category20b = function() { return d3.scale.ordinal().range(d3_category20b); }; d3.scale.category20c = function() { return d3.scale.ordinal().range(d3_category20c); }; var d3_category10 = [ 2062260, 16744206, 2924588, 14034728, 9725885, 9197131, 14907330, 8355711, 12369186, 1556175 ].map(d3_rgbString); var d3_category20 = [ 2062260, 11454440, 16744206, 16759672, 2924588, 10018698, 14034728, 16750742, 9725885, 12955861, 9197131, 12885140, 14907330, 16234194, 8355711, 13092807, 12369186, 14408589, 1556175, 10410725 ].map(d3_rgbString); var d3_category20b = [ 3750777, 5395619, 7040719, 10264286, 6519097, 9216594, 11915115, 13556636, 9202993, 12426809, 15186514, 15190932, 8666169, 11356490, 14049643, 15177372, 8077683, 10834324, 13528509, 14589654 ].map(d3_rgbString); var d3_category20c = [ 3244733, 7057110, 10406625, 13032431, 15095053, 16616764, 16625259, 16634018, 3253076, 7652470, 10607003, 13101504, 7695281, 10394312, 12369372, 14342891, 6513507, 9868950, 12434877, 14277081 ].map(d3_rgbString); d3.scale.quantile = function() { return d3_scale_quantile([], []); }; function d3_scale_quantile(domain, range) { var thresholds; function rescale() { var k = 0, q = range.length; thresholds = []; while (++k < q) thresholds[k - 1] = d3.quantile(domain, k / q); return scale; } function scale(x) { if (!isNaN(x = +x)) return range[d3.bisect(thresholds, x)]; } scale.domain = function(x) { if (!arguments.length) return domain; domain = x.map(d3_number).filter(d3_numeric).sort(d3_ascending); return rescale(); }; scale.range = function(x) { if (!arguments.length) return range; range = x; return rescale(); }; scale.quantiles = function() { return thresholds; }; scale.invertExtent = function(y) { y = range.indexOf(y); return y < 0 ? [ NaN, NaN ] : [ y > 0 ? thresholds[y - 1] : domain[0], y < thresholds.length ? thresholds[y] : domain[domain.length - 1] ]; }; scale.copy = function() { return d3_scale_quantile(domain, range); }; return rescale(); } d3.scale.quantize = function() { return d3_scale_quantize(0, 1, [ 0, 1 ]); }; function d3_scale_quantize(x0, x1, range) { var kx, i; function scale(x) { return range[Math.max(0, Math.min(i, Math.floor(kx * (x - x0))))]; } function rescale() { kx = range.length / (x1 - x0); i = range.length - 1; return scale; } scale.domain = function(x) { if (!arguments.length) return [ x0, x1 ]; x0 = +x[0]; x1 = +x[x.length - 1]; return rescale(); }; scale.range = function(x) { if (!arguments.length) return range; range = x; return rescale(); }; scale.invertExtent = function(y) { y = range.indexOf(y); y = y < 0 ? NaN : y / kx + x0; return [ y, y + 1 / kx ]; }; scale.copy = function() { return d3_scale_quantize(x0, x1, range); }; return rescale(); } d3.scale.threshold = function() { return d3_scale_threshold([ .5 ], [ 0, 1 ]); }; function d3_scale_threshold(domain, range) { function scale(x) { if (x <= x) return range[d3.bisect(domain, x)]; } scale.domain = function(_) { if (!arguments.length) return domain; domain = _; return scale; }; scale.range = function(_) { if (!arguments.length) return range; range = _; return scale; }; scale.invertExtent = function(y) { y = range.indexOf(y); return [ domain[y - 1], domain[y] ]; }; scale.copy = function() { return d3_scale_threshold(domain, range); }; return scale; } d3.scale.identity = function() { return d3_scale_identity([ 0, 1 ]); }; function d3_scale_identity(domain) { function identity(x) { return +x; } identity.invert = identity; identity.domain = identity.range = function(x) { if (!arguments.length) return domain; domain = x.map(identity); return identity; }; identity.ticks = function(m) { return d3_scale_linearTicks(domain, m); }; identity.tickFormat = function(m, format) { return d3_scale_linearTickFormat(domain, m, format); }; identity.copy = function() { return d3_scale_identity(domain); }; return identity; } d3.svg = {}; function d3_zero() { return 0; } d3.svg.arc = function() { var innerRadius = d3_svg_arcInnerRadius, outerRadius = d3_svg_arcOuterRadius, cornerRadius = d3_zero, padRadius = d3_svg_arcAuto, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle, padAngle = d3_svg_arcPadAngle; function arc() { var r0 = Math.max(0, +innerRadius.apply(this, arguments)), r1 = Math.max(0, +outerRadius.apply(this, arguments)), a0 = startAngle.apply(this, arguments) - halfπ, a1 = endAngle.apply(this, arguments) - halfπ, da = Math.abs(a1 - a0), cw = a0 > a1 ? 0 : 1; if (r1 < r0) rc = r1, r1 = r0, r0 = rc; if (da >= τε) return circleSegment(r1, cw) + (r0 ? circleSegment(r0, 1 - cw) : "") + "Z"; var rc, cr, rp, ap, p0 = 0, p1 = 0, x0, y0, x1, y1, x2, y2, x3, y3, path = []; if (ap = (+padAngle.apply(this, arguments) || 0) / 2) { rp = padRadius === d3_svg_arcAuto ? Math.sqrt(r0 * r0 + r1 * r1) : +padRadius.apply(this, arguments); if (!cw) p1 *= -1; if (r1) p1 = d3_asin(rp / r1 * Math.sin(ap)); if (r0) p0 = d3_asin(rp / r0 * Math.sin(ap)); } if (r1) { x0 = r1 * Math.cos(a0 + p1); y0 = r1 * Math.sin(a0 + p1); x1 = r1 * Math.cos(a1 - p1); y1 = r1 * Math.sin(a1 - p1); var l1 = Math.abs(a1 - a0 - 2 * p1) <= π ? 0 : 1; if (p1 && d3_svg_arcSweep(x0, y0, x1, y1) === cw ^ l1) { var h1 = (a0 + a1) / 2; x0 = r1 * Math.cos(h1); y0 = r1 * Math.sin(h1); x1 = y1 = null; } } else { x0 = y0 = 0; } if (r0) { x2 = r0 * Math.cos(a1 - p0); y2 = r0 * Math.sin(a1 - p0); x3 = r0 * Math.cos(a0 + p0); y3 = r0 * Math.sin(a0 + p0); var l0 = Math.abs(a0 - a1 + 2 * p0) <= π ? 0 : 1; if (p0 && d3_svg_arcSweep(x2, y2, x3, y3) === 1 - cw ^ l0) { var h0 = (a0 + a1) / 2; x2 = r0 * Math.cos(h0); y2 = r0 * Math.sin(h0); x3 = y3 = null; } } else { x2 = y2 = 0; } if ((rc = Math.min(Math.abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments))) > .001) { cr = r0 < r1 ^ cw ? 0 : 1; var oc = x3 == null ? [ x2, y2 ] : x1 == null ? [ x0, y0 ] : d3_geom_polygonIntersect([ x0, y0 ], [ x3, y3 ], [ x1, y1 ], [ x2, y2 ]), ax = x0 - oc[0], ay = y0 - oc[1], bx = x1 - oc[0], by = y1 - oc[1], kc = 1 / Math.sin(Math.acos((ax * bx + ay * by) / (Math.sqrt(ax * ax + ay * ay) * Math.sqrt(bx * bx + by * by))) / 2), lc = Math.sqrt(oc[0] * oc[0] + oc[1] * oc[1]); if (x1 != null) { var rc1 = Math.min(rc, (r1 - lc) / (kc + 1)), t30 = d3_svg_arcCornerTangents(x3 == null ? [ x2, y2 ] : [ x3, y3 ], [ x0, y0 ], r1, rc1, cw), t12 = d3_svg_arcCornerTangents([ x1, y1 ], [ x2, y2 ], r1, rc1, cw); if (rc === rc1) { path.push("M", t30[0], "A", rc1, ",", rc1, " 0 0,", cr, " ", t30[1], "A", r1, ",", r1, " 0 ", 1 - cw ^ d3_svg_arcSweep(t30[1][0], t30[1][1], t12[1][0], t12[1][1]), ",", cw, " ", t12[1], "A", rc1, ",", rc1, " 0 0,", cr, " ", t12[0]); } else { path.push("M", t30[0], "A", rc1, ",", rc1, " 0 1,", cr, " ", t12[0]); } } else { path.push("M", x0, ",", y0); } if (x3 != null) { var rc0 = Math.min(rc, (r0 - lc) / (kc - 1)), t03 = d3_svg_arcCornerTangents([ x0, y0 ], [ x3, y3 ], r0, -rc0, cw), t21 = d3_svg_arcCornerTangents([ x2, y2 ], x1 == null ? [ x0, y0 ] : [ x1, y1 ], r0, -rc0, cw); if (rc === rc0) { path.push("L", t21[0], "A", rc0, ",", rc0, " 0 0,", cr, " ", t21[1], "A", r0, ",", r0, " 0 ", cw ^ d3_svg_arcSweep(t21[1][0], t21[1][1], t03[1][0], t03[1][1]), ",", 1 - cw, " ", t03[1], "A", rc0, ",", rc0, " 0 0,", cr, " ", t03[0]); } else { path.push("L", t21[0], "A", rc0, ",", rc0, " 0 0,", cr, " ", t03[0]); } } else { path.push("L", x2, ",", y2); } } else { path.push("M", x0, ",", y0); if (x1 != null) path.push("A", r1, ",", r1, " 0 ", l1, ",", cw, " ", x1, ",", y1); path.push("L", x2, ",", y2); if (x3 != null) path.push("A", r0, ",", r0, " 0 ", l0, ",", 1 - cw, " ", x3, ",", y3); } path.push("Z"); return path.join(""); } function circleSegment(r1, cw) { return "M0," + r1 + "A" + r1 + "," + r1 + " 0 1," + cw + " 0," + -r1 + "A" + r1 + "," + r1 + " 0 1," + cw + " 0," + r1; } arc.innerRadius = function(v) { if (!arguments.length) return innerRadius; innerRadius = d3_functor(v); return arc; }; arc.outerRadius = function(v) { if (!arguments.length) return outerRadius; outerRadius = d3_functor(v); return arc; }; arc.cornerRadius = function(v) { if (!arguments.length) return cornerRadius; cornerRadius = d3_functor(v); return arc; }; arc.padRadius = function(v) { if (!arguments.length) return padRadius; padRadius = v == d3_svg_arcAuto ? d3_svg_arcAuto : d3_functor(v); return arc; }; arc.startAngle = function(v) { if (!arguments.length) return startAngle; startAngle = d3_functor(v); return arc; }; arc.endAngle = function(v) { if (!arguments.length) return endAngle; endAngle = d3_functor(v); return arc; }; arc.padAngle = function(v) { if (!arguments.length) return padAngle; padAngle = d3_functor(v); return arc; }; arc.centroid = function() { var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2, a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - halfπ; return [ Math.cos(a) * r, Math.sin(a) * r ]; }; return arc; }; var d3_svg_arcAuto = "auto"; function d3_svg_arcInnerRadius(d) { return d.innerRadius; } function d3_svg_arcOuterRadius(d) { return d.outerRadius; } function d3_svg_arcStartAngle(d) { return d.startAngle; } function d3_svg_arcEndAngle(d) { return d.endAngle; } function d3_svg_arcPadAngle(d) { return d && d.padAngle; } function d3_svg_arcSweep(x0, y0, x1, y1) { return (x0 - x1) * y0 - (y0 - y1) * x0 > 0 ? 0 : 1; } function d3_svg_arcCornerTangents(p0, p1, r1, rc, cw) { var x01 = p0[0] - p1[0], y01 = p0[1] - p1[1], lo = (cw ? rc : -rc) / Math.sqrt(x01 * x01 + y01 * y01), ox = lo * y01, oy = -lo * x01, x1 = p0[0] + ox, y1 = p0[1] + oy, x2 = p1[0] + ox, y2 = p1[1] + oy, x3 = (x1 + x2) / 2, y3 = (y1 + y2) / 2, dx = x2 - x1, dy = y2 - y1, d2 = dx * dx + dy * dy, r = r1 - rc, D = x1 * y2 - x2 * y1, d = (dy < 0 ? -1 : 1) * Math.sqrt(r * r * d2 - D * D), cx0 = (D * dy - dx * d) / d2, cy0 = (-D * dx - dy * d) / d2, cx1 = (D * dy + dx * d) / d2, cy1 = (-D * dx + dy * d) / d2, dx0 = cx0 - x3, dy0 = cy0 - y3, dx1 = cx1 - x3, dy1 = cy1 - y3; if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1; return [ [ cx0 - ox, cy0 - oy ], [ cx0 * r1 / r, cy0 * r1 / r ] ]; } function d3_svg_line(projection) { var x = d3_geom_pointX, y = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, tension = .7; function line(data) { var segments = [], points = [], i = -1, n = data.length, d, fx = d3_functor(x), fy = d3_functor(y); function segment() { segments.push("M", interpolate(projection(points), tension)); } while (++i < n) { if (defined.call(this, d = data[i], i)) { points.push([ +fx.call(this, d, i), +fy.call(this, d, i) ]); } else if (points.length) { segment(); points = []; } } if (points.length) segment(); return segments.length ? segments.join("") : null; } line.x = function(_) { if (!arguments.length) return x; x = _; return line; }; line.y = function(_) { if (!arguments.length) return y; y = _; return line; }; line.defined = function(_) { if (!arguments.length) return defined; defined = _; return line; }; line.interpolate = function(_) { if (!arguments.length) return interpolateKey; if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key; return line; }; line.tension = function(_) { if (!arguments.length) return tension; tension = _; return line; }; return line; } d3.svg.line = function() { return d3_svg_line(d3_identity); }; var d3_svg_lineInterpolators = d3.map({ linear: d3_svg_lineLinear, "linear-closed": d3_svg_lineLinearClosed, step: d3_svg_lineStep, "step-before": d3_svg_lineStepBefore, "step-after": d3_svg_lineStepAfter, basis: d3_svg_lineBasis, "basis-open": d3_svg_lineBasisOpen, "basis-closed": d3_svg_lineBasisClosed, bundle: d3_svg_lineBundle, cardinal: d3_svg_lineCardinal, "cardinal-open": d3_svg_lineCardinalOpen, "cardinal-closed": d3_svg_lineCardinalClosed, monotone: d3_svg_lineMonotone }); d3_svg_lineInterpolators.forEach(function(key, value) { value.key = key; value.closed = /-closed$/.test(key); }); function d3_svg_lineLinear(points) { return points.join("L"); } function d3_svg_lineLinearClosed(points) { return d3_svg_lineLinear(points) + "Z"; } function d3_svg_lineStep(points) { var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; while (++i < n) path.push("H", (p[0] + (p = points[i])[0]) / 2, "V", p[1]); if (n > 1) path.push("H", p[0]); return path.join(""); } function d3_svg_lineStepBefore(points) { var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; while (++i < n) path.push("V", (p = points[i])[1], "H", p[0]); return path.join(""); } function d3_svg_lineStepAfter(points) { var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; while (++i < n) path.push("H", (p = points[i])[0], "V", p[1]); return path.join(""); } function d3_svg_lineCardinalOpen(points, tension) { return points.length < 4 ? d3_svg_lineLinear(points) : points[1] + d3_svg_lineHermite(points.slice(1, -1), d3_svg_lineCardinalTangents(points, tension)); } function d3_svg_lineCardinalClosed(points, tension) { return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite((points.push(points[0]), points), d3_svg_lineCardinalTangents([ points[points.length - 2] ].concat(points, [ points[1] ]), tension)); } function d3_svg_lineCardinal(points, tension) { return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineCardinalTangents(points, tension)); } function d3_svg_lineHermite(points, tangents) { if (tangents.length < 1 || points.length != tangents.length && points.length != tangents.length + 2) { return d3_svg_lineLinear(points); } var quad = points.length != tangents.length, path = "", p0 = points[0], p = points[1], t0 = tangents[0], t = t0, pi = 1; if (quad) { path += "Q" + (p[0] - t0[0] * 2 / 3) + "," + (p[1] - t0[1] * 2 / 3) + "," + p[0] + "," + p[1]; p0 = points[1]; pi = 2; } if (tangents.length > 1) { t = tangents[1]; p = points[pi]; pi++; path += "C" + (p0[0] + t0[0]) + "," + (p0[1] + t0[1]) + "," + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1]; for (var i = 2; i < tangents.length; i++, pi++) { p = points[pi]; t = tangents[i]; path += "S" + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1]; } } if (quad) { var lp = points[pi]; path += "Q" + (p[0] + t[0] * 2 / 3) + "," + (p[1] + t[1] * 2 / 3) + "," + lp[0] + "," + lp[1]; } return path; } function d3_svg_lineCardinalTangents(points, tension) { var tangents = [], a = (1 - tension) / 2, p0, p1 = points[0], p2 = points[1], i = 1, n = points.length; while (++i < n) { p0 = p1; p1 = p2; p2 = points[i]; tangents.push([ a * (p2[0] - p0[0]), a * (p2[1] - p0[1]) ]); } return tangents; } function d3_svg_lineBasis(points) { if (points.length < 3) return d3_svg_lineLinear(points); var i = 1, n = points.length, pi = points[0], x0 = pi[0], y0 = pi[1], px = [ x0, x0, x0, (pi = points[1])[0] ], py = [ y0, y0, y0, pi[1] ], path = [ x0, ",", y0, "L", d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ]; points.push(points[n - 1]); while (++i <= n) { pi = points[i]; px.shift(); px.push(pi[0]); py.shift(); py.push(pi[1]); d3_svg_lineBasisBezier(path, px, py); } points.pop(); path.push("L", pi); return path.join(""); } function d3_svg_lineBasisOpen(points) { if (points.length < 4) return d3_svg_lineLinear(points); var path = [], i = -1, n = points.length, pi, px = [ 0 ], py = [ 0 ]; while (++i < 3) { pi = points[i]; px.push(pi[0]); py.push(pi[1]); } path.push(d3_svg_lineDot4(d3_svg_lineBasisBezier3, px) + "," + d3_svg_lineDot4(d3_svg_lineBasisBezier3, py)); --i; while (++i < n) { pi = points[i]; px.shift(); px.push(pi[0]); py.shift(); py.push(pi[1]); d3_svg_lineBasisBezier(path, px, py); } return path.join(""); } function d3_svg_lineBasisClosed(points) { var path, i = -1, n = points.length, m = n + 4, pi, px = [], py = []; while (++i < 4) { pi = points[i % n]; px.push(pi[0]); py.push(pi[1]); } path = [ d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ]; --i; while (++i < m) { pi = points[i % n]; px.shift(); px.push(pi[0]); py.shift(); py.push(pi[1]); d3_svg_lineBasisBezier(path, px, py); } return path.join(""); } function d3_svg_lineBundle(points, tension) { var n = points.length - 1; if (n) { var x0 = points[0][0], y0 = points[0][1], dx = points[n][0] - x0, dy = points[n][1] - y0, i = -1, p, t; while (++i <= n) { p = points[i]; t = i / n; p[0] = tension * p[0] + (1 - tension) * (x0 + t * dx); p[1] = tension * p[1] + (1 - tension) * (y0 + t * dy); } } return d3_svg_lineBasis(points); } function d3_svg_lineDot4(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; } var d3_svg_lineBasisBezier1 = [ 0, 2 / 3, 1 / 3, 0 ], d3_svg_lineBasisBezier2 = [ 0, 1 / 3, 2 / 3, 0 ], d3_svg_lineBasisBezier3 = [ 0, 1 / 6, 2 / 3, 1 / 6 ]; function d3_svg_lineBasisBezier(path, x, y) { path.push("C", d3_svg_lineDot4(d3_svg_lineBasisBezier1, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier1, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, y)); } function d3_svg_lineSlope(p0, p1) { return (p1[1] - p0[1]) / (p1[0] - p0[0]); } function d3_svg_lineFiniteDifferences(points) { var i = 0, j = points.length - 1, m = [], p0 = points[0], p1 = points[1], d = m[0] = d3_svg_lineSlope(p0, p1); while (++i < j) { m[i] = (d + (d = d3_svg_lineSlope(p0 = p1, p1 = points[i + 1]))) / 2; } m[i] = d; return m; } function d3_svg_lineMonotoneTangents(points) { var tangents = [], d, a, b, s, m = d3_svg_lineFiniteDifferences(points), i = -1, j = points.length - 1; while (++i < j) { d = d3_svg_lineSlope(points[i], points[i + 1]); if (abs(d) < ε) { m[i] = m[i + 1] = 0; } else { a = m[i] / d; b = m[i + 1] / d; s = a * a + b * b; if (s > 9) { s = d * 3 / Math.sqrt(s); m[i] = s * a; m[i + 1] = s * b; } } } i = -1; while (++i <= j) { s = (points[Math.min(j, i + 1)][0] - points[Math.max(0, i - 1)][0]) / (6 * (1 + m[i] * m[i])); tangents.push([ s || 0, m[i] * s || 0 ]); } return tangents; } function d3_svg_lineMonotone(points) { return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points)); } d3.svg.line.radial = function() { var line = d3_svg_line(d3_svg_lineRadial); line.radius = line.x, delete line.x; line.angle = line.y, delete line.y; return line; }; function d3_svg_lineRadial(points) { var point, i = -1, n = points.length, r, a; while (++i < n) { point = points[i]; r = point[0]; a = point[1] - halfπ; point[0] = r * Math.cos(a); point[1] = r * Math.sin(a); } return points; } function d3_svg_area(projection) { var x0 = d3_geom_pointX, x1 = d3_geom_pointX, y0 = 0, y1 = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, interpolateReverse = interpolate, L = "L", tension = .7; function area(data) { var segments = [], points0 = [], points1 = [], i = -1, n = data.length, d, fx0 = d3_functor(x0), fy0 = d3_functor(y0), fx1 = x0 === x1 ? function() { return x; } : d3_functor(x1), fy1 = y0 === y1 ? function() { return y; } : d3_functor(y1), x, y; function segment() { segments.push("M", interpolate(projection(points1), tension), L, interpolateReverse(projection(points0.reverse()), tension), "Z"); } while (++i < n) { if (defined.call(this, d = data[i], i)) { points0.push([ x = +fx0.call(this, d, i), y = +fy0.call(this, d, i) ]); points1.push([ +fx1.call(this, d, i), +fy1.call(this, d, i) ]); } else if (points0.length) { segment(); points0 = []; points1 = []; } } if (points0.length) segment(); return segments.length ? segments.join("") : null; } area.x = function(_) { if (!arguments.length) return x1; x0 = x1 = _; return area; }; area.x0 = function(_) { if (!arguments.length) return x0; x0 = _; return area; }; area.x1 = function(_) { if (!arguments.length) return x1; x1 = _; return area; }; area.y = function(_) { if (!arguments.length) return y1; y0 = y1 = _; return area; }; area.y0 = function(_) { if (!arguments.length) return y0; y0 = _; return area; }; area.y1 = function(_) { if (!arguments.length) return y1; y1 = _; return area; }; area.defined = function(_) { if (!arguments.length) return defined; defined = _; return area; }; area.interpolate = function(_) { if (!arguments.length) return interpolateKey; if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key; interpolateReverse = interpolate.reverse || interpolate; L = interpolate.closed ? "M" : "L"; return area; }; area.tension = function(_) { if (!arguments.length) return tension; tension = _; return area; }; return area; } d3_svg_lineStepBefore.reverse = d3_svg_lineStepAfter; d3_svg_lineStepAfter.reverse = d3_svg_lineStepBefore; d3.svg.area = function() { return d3_svg_area(d3_identity); }; d3.svg.area.radial = function() { var area = d3_svg_area(d3_svg_lineRadial); area.radius = area.x, delete area.x; area.innerRadius = area.x0, delete area.x0; area.outerRadius = area.x1, delete area.x1; area.angle = area.y, delete area.y; area.startAngle = area.y0, delete area.y0; area.endAngle = area.y1, delete area.y1; return area; }; d3.svg.chord = function() { var source = d3_source, target = d3_target, radius = d3_svg_chordRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle; function chord(d, i) { var s = subgroup(this, source, d, i), t = subgroup(this, target, d, i); return "M" + s.p0 + arc(s.r, s.p1, s.a1 - s.a0) + (equals(s, t) ? curve(s.r, s.p1, s.r, s.p0) : curve(s.r, s.p1, t.r, t.p0) + arc(t.r, t.p1, t.a1 - t.a0) + curve(t.r, t.p1, s.r, s.p0)) + "Z"; } function subgroup(self, f, d, i) { var subgroup = f.call(self, d, i), r = radius.call(self, subgroup, i), a0 = startAngle.call(self, subgroup, i) - halfπ, a1 = endAngle.call(self, subgroup, i) - halfπ; return { r: r, a0: a0, a1: a1, p0: [ r * Math.cos(a0), r * Math.sin(a0) ], p1: [ r * Math.cos(a1), r * Math.sin(a1) ] }; } function equals(a, b) { return a.a0 == b.a0 && a.a1 == b.a1; } function arc(r, p, a) { return "A" + r + "," + r + " 0 " + +(a > π) + ",1 " + p; } function curve(r0, p0, r1, p1) { return "Q 0,0 " + p1; } chord.radius = function(v) { if (!arguments.length) return radius; radius = d3_functor(v); return chord; }; chord.source = function(v) { if (!arguments.length) return source; source = d3_functor(v); return chord; }; chord.target = function(v) { if (!arguments.length) return target; target = d3_functor(v); return chord; }; chord.startAngle = function(v) { if (!arguments.length) return startAngle; startAngle = d3_functor(v); return chord; }; chord.endAngle = function(v) { if (!arguments.length) return endAngle; endAngle = d3_functor(v); return chord; }; return chord; }; function d3_svg_chordRadius(d) { return d.radius; } d3.svg.diagonal = function() { var source = d3_source, target = d3_target, projection = d3_svg_diagonalProjection; function diagonal(d, i) { var p0 = source.call(this, d, i), p3 = target.call(this, d, i), m = (p0.y + p3.y) / 2, p = [ p0, { x: p0.x, y: m }, { x: p3.x, y: m }, p3 ]; p = p.map(projection); return "M" + p[0] + "C" + p[1] + " " + p[2] + " " + p[3]; } diagonal.source = function(x) { if (!arguments.length) return source; source = d3_functor(x); return diagonal; }; diagonal.target = function(x) { if (!arguments.length) return target; target = d3_functor(x); return diagonal; }; diagonal.projection = function(x) { if (!arguments.length) return projection; projection = x; return diagonal; }; return diagonal; }; function d3_svg_diagonalProjection(d) { return [ d.x, d.y ]; } d3.svg.diagonal.radial = function() { var diagonal = d3.svg.diagonal(), projection = d3_svg_diagonalProjection, projection_ = diagonal.projection; diagonal.projection = function(x) { return arguments.length ? projection_(d3_svg_diagonalRadialProjection(projection = x)) : projection; }; return diagonal; }; function d3_svg_diagonalRadialProjection(projection) { return function() { var d = projection.apply(this, arguments), r = d[0], a = d[1] - halfπ; return [ r * Math.cos(a), r * Math.sin(a) ]; }; } d3.svg.symbol = function() { var type = d3_svg_symbolType, size = d3_svg_symbolSize; function symbol(d, i) { return (d3_svg_symbols.get(type.call(this, d, i)) || d3_svg_symbolCircle)(size.call(this, d, i)); } symbol.type = function(x) { if (!arguments.length) return type; type = d3_functor(x); return symbol; }; symbol.size = function(x) { if (!arguments.length) return size; size = d3_functor(x); return symbol; }; return symbol; }; function d3_svg_symbolSize() { return 64; } function d3_svg_symbolType() { return "circle"; } function d3_svg_symbolCircle(size) { var r = Math.sqrt(size / π); return "M0," + r + "A" + r + "," + r + " 0 1,1 0," + -r + "A" + r + "," + r + " 0 1,1 0," + r + "Z"; } var d3_svg_symbols = d3.map({ circle: d3_svg_symbolCircle, cross: function(size) { var r = Math.sqrt(size / 5) / 2; return "M" + -3 * r + "," + -r + "H" + -r + "V" + -3 * r + "H" + r + "V" + -r + "H" + 3 * r + "V" + r + "H" + r + "V" + 3 * r + "H" + -r + "V" + r + "H" + -3 * r + "Z"; }, diamond: function(size) { var ry = Math.sqrt(size / (2 * d3_svg_symbolTan30)), rx = ry * d3_svg_symbolTan30; return "M0," + -ry + "L" + rx + ",0" + " 0," + ry + " " + -rx + ",0" + "Z"; }, square: function(size) { var r = Math.sqrt(size) / 2; return "M" + -r + "," + -r + "L" + r + "," + -r + " " + r + "," + r + " " + -r + "," + r + "Z"; }, "triangle-down": function(size) { var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2; return "M0," + ry + "L" + rx + "," + -ry + " " + -rx + "," + -ry + "Z"; }, "triangle-up": function(size) { var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2; return "M0," + -ry + "L" + rx + "," + ry + " " + -rx + "," + ry + "Z"; } }); d3.svg.symbolTypes = d3_svg_symbols.keys(); var d3_svg_symbolSqrt3 = Math.sqrt(3), d3_svg_symbolTan30 = Math.tan(30 * d3_radians); d3_selectionPrototype.transition = function(name) { var id = d3_transitionInheritId || ++d3_transitionId, ns = d3_transitionNamespace(name), subgroups = [], subgroup, node, transition = d3_transitionInherit || { time: Date.now(), ease: d3_ease_cubicInOut, delay: 0, duration: 250 }; for (var j = -1, m = this.length; ++j < m; ) { subgroups.push(subgroup = []); for (var group = this[j], i = -1, n = group.length; ++i < n; ) { if (node = group[i]) d3_transitionNode(node, i, ns, id, transition); subgroup.push(node); } } return d3_transition(subgroups, ns, id); }; d3_selectionPrototype.interrupt = function(name) { return this.each(name == null ? d3_selection_interrupt : d3_selection_interruptNS(d3_transitionNamespace(name))); }; var d3_selection_interrupt = d3_selection_interruptNS(d3_transitionNamespace()); function d3_selection_interruptNS(ns) { return function() { var lock, active; if ((lock = this[ns]) && (active = lock[lock.active])) { if (--lock.count) { delete lock[lock.active]; lock.active += .5; } else { delete this[ns]; } active.event && active.event.interrupt.call(this, this.__data__, active.index); } }; } function d3_transition(groups, ns, id) { d3_subclass(groups, d3_transitionPrototype); groups.namespace = ns; groups.id = id; return groups; } var d3_transitionPrototype = [], d3_transitionId = 0, d3_transitionInheritId, d3_transitionInherit; d3_transitionPrototype.call = d3_selectionPrototype.call; d3_transitionPrototype.empty = d3_selectionPrototype.empty; d3_transitionPrototype.node = d3_selectionPrototype.node; d3_transitionPrototype.size = d3_selectionPrototype.size; d3.transition = function(selection, name) { return selection && selection.transition ? d3_transitionInheritId ? selection.transition(name) : selection : d3_selectionRoot.transition(selection); }; d3.transition.prototype = d3_transitionPrototype; d3_transitionPrototype.select = function(selector) { var id = this.id, ns = this.namespace, subgroups = [], subgroup, subnode, node; selector = d3_selection_selector(selector); for (var j = -1, m = this.length; ++j < m; ) { subgroups.push(subgroup = []); for (var group = this[j], i = -1, n = group.length; ++i < n; ) { if ((node = group[i]) && (subnode = selector.call(node, node.__data__, i, j))) { if ("__data__" in node) subnode.__data__ = node.__data__; d3_transitionNode(subnode, i, ns, id, node[ns][id]); subgroup.push(subnode); } else { subgroup.push(null); } } } return d3_transition(subgroups, ns, id); }; d3_transitionPrototype.selectAll = function(selector) { var id = this.id, ns = this.namespace, subgroups = [], subgroup, subnodes, node, subnode, transition; selector = d3_selection_selectorAll(selector); for (var j = -1, m = this.length; ++j < m; ) { for (var group = this[j], i = -1, n = group.length; ++i < n; ) { if (node = group[i]) { transition = node[ns][id]; subnodes = selector.call(node, node.__data__, i, j); subgroups.push(subgroup = []); for (var k = -1, o = subnodes.length; ++k < o; ) { if (subnode = subnodes[k]) d3_transitionNode(subnode, k, ns, id, transition); subgroup.push(subnode); } } } } return d3_transition(subgroups, ns, id); }; d3_transitionPrototype.filter = function(filter) { var subgroups = [], subgroup, group, node; if (typeof filter !== "function") filter = d3_selection_filter(filter); for (var j = 0, m = this.length; j < m; j++) { subgroups.push(subgroup = []); for (var group = this[j], i = 0, n = group.length; i < n; i++) { if ((node = group[i]) && filter.call(node, node.__data__, i, j)) { subgroup.push(node); } } } return d3_transition(subgroups, this.namespace, this.id); }; d3_transitionPrototype.tween = function(name, tween) { var id = this.id, ns = this.namespace; if (arguments.length < 2) return this.node()[ns][id].tween.get(name); return d3_selection_each(this, tween == null ? function(node) { node[ns][id].tween.remove(name); } : function(node) { node[ns][id].tween.set(name, tween); }); }; function d3_transition_tween(groups, name, value, tween) { var id = groups.id, ns = groups.namespace; return d3_selection_each(groups, typeof value === "function" ? function(node, i, j) { node[ns][id].tween.set(name, tween(value.call(node, node.__data__, i, j))); } : (value = tween(value), function(node) { node[ns][id].tween.set(name, value); })); } d3_transitionPrototype.attr = function(nameNS, value) { if (arguments.length < 2) { for (value in nameNS) this.attr(value, nameNS[value]); return this; } var interpolate = nameNS == "transform" ? d3_interpolateTransform : d3_interpolate, name = d3.ns.qualify(nameNS); function attrNull() { this.removeAttribute(name); } function attrNullNS() { this.removeAttributeNS(name.space, name.local); } function attrTween(b) { return b == null ? attrNull : (b += "", function() { var a = this.getAttribute(name), i; return a !== b && (i = interpolate(a, b), function(t) { this.setAttribute(name, i(t)); }); }); } function attrTweenNS(b) { return b == null ? attrNullNS : (b += "", function() { var a = this.getAttributeNS(name.space, name.local), i; return a !== b && (i = interpolate(a, b), function(t) { this.setAttributeNS(name.space, name.local, i(t)); }); }); } return d3_transition_tween(this, "attr." + nameNS, value, name.local ? attrTweenNS : attrTween); }; d3_transitionPrototype.attrTween = function(nameNS, tween) { var name = d3.ns.qualify(nameNS); function attrTween(d, i) { var f = tween.call(this, d, i, this.getAttribute(name)); return f && function(t) { this.setAttribute(name, f(t)); }; } function attrTweenNS(d, i) { var f = tween.call(this, d, i, this.getAttributeNS(name.space, name.local)); return f && function(t) { this.setAttributeNS(name.space, name.local, f(t)); }; } return this.tween("attr." + nameNS, name.local ? attrTweenNS : attrTween); }; d3_transitionPrototype.style = function(name, value, priority) { var n = arguments.length; if (n < 3) { if (typeof name !== "string") { if (n < 2) value = ""; for (priority in name) this.style(priority, name[priority], value); return this; } priority = ""; } function styleNull() { this.style.removeProperty(name); } function styleString(b) { return b == null ? styleNull : (b += "", function() { var a = d3_window.getComputedStyle(this, null).getPropertyValue(name), i; return a !== b && (i = d3_interpolate(a, b), function(t) { this.style.setProperty(name, i(t), priority); }); }); } return d3_transition_tween(this, "style." + name, value, styleString); }; d3_transitionPrototype.styleTween = function(name, tween, priority) { if (arguments.length < 3) priority = ""; function styleTween(d, i) { var f = tween.call(this, d, i, d3_window.getComputedStyle(this, null).getPropertyValue(name)); return f && function(t) { this.style.setProperty(name, f(t), priority); }; } return this.tween("style." + name, styleTween); }; d3_transitionPrototype.text = function(value) { return d3_transition_tween(this, "text", value, d3_transition_text); }; function d3_transition_text(b) { if (b == null) b = ""; return function() { this.textContent = b; }; } d3_transitionPrototype.remove = function() { var ns = this.namespace; return this.each("end.transition", function() { var p; if (this[ns].count < 2 && (p = this.parentNode)) p.removeChild(this); }); }; d3_transitionPrototype.ease = function(value) { var id = this.id, ns = this.namespace; if (arguments.length < 1) return this.node()[ns][id].ease; if (typeof value !== "function") value = d3.ease.apply(d3, arguments); return d3_selection_each(this, function(node) { node[ns][id].ease = value; }); }; d3_transitionPrototype.delay = function(value) { var id = this.id, ns = this.namespace; if (arguments.length < 1) return this.node()[ns][id].delay; return d3_selection_each(this, typeof value === "function" ? function(node, i, j) { node[ns][id].delay = +value.call(node, node.__data__, i, j); } : (value = +value, function(node) { node[ns][id].delay = value; })); }; d3_transitionPrototype.duration = function(value) { var id = this.id, ns = this.namespace; if (arguments.length < 1) return this.node()[ns][id].duration; return d3_selection_each(this, typeof value === "function" ? function(node, i, j) { node[ns][id].duration = Math.max(1, value.call(node, node.__data__, i, j)); } : (value = Math.max(1, value), function(node) { node[ns][id].duration = value; })); }; d3_transitionPrototype.each = function(type, listener) { var id = this.id, ns = this.namespace; if (arguments.length < 2) { var inherit = d3_transitionInherit, inheritId = d3_transitionInheritId; try { d3_transitionInheritId = id; d3_selection_each(this, function(node, i, j) { d3_transitionInherit = node[ns][id]; type.call(node, node.__data__, i, j); }); } finally { d3_transitionInherit = inherit; d3_transitionInheritId = inheritId; } } else { d3_selection_each(this, function(node) { var transition = node[ns][id]; (transition.event || (transition.event = d3.dispatch("start", "end", "interrupt"))).on(type, listener); }); } return this; }; d3_transitionPrototype.transition = function() { var id0 = this.id, id1 = ++d3_transitionId, ns = this.namespace, subgroups = [], subgroup, group, node, transition; for (var j = 0, m = this.length; j < m; j++) { subgroups.push(subgroup = []); for (var group = this[j], i = 0, n = group.length; i < n; i++) { if (node = group[i]) { transition = node[ns][id0]; d3_transitionNode(node, i, ns, id1, { time: transition.time, ease: transition.ease, delay: transition.delay + transition.duration, duration: transition.duration }); } subgroup.push(node); } } return d3_transition(subgroups, ns, id1); }; function d3_transitionNamespace(name) { return name == null ? "__transition__" : "__transition_" + name + "__"; } function d3_transitionNode(node, i, ns, id, inherit) { var lock = node[ns] || (node[ns] = { active: 0, count: 0 }), transition = lock[id]; if (!transition) { var time = inherit.time; transition = lock[id] = { tween: new d3_Map(), time: time, delay: inherit.delay, duration: inherit.duration, ease: inherit.ease, index: i }; inherit = null; ++lock.count; d3.timer(function(elapsed) { var delay = transition.delay, duration, ease, timer = d3_timer_active, tweened = []; timer.t = delay + time; if (delay <= elapsed) return start(elapsed - delay); timer.c = start; function start(elapsed) { if (lock.active > id) return stop(); var active = lock[lock.active]; if (active) { --lock.count; delete lock[lock.active]; active.event && active.event.interrupt.call(node, node.__data__, active.index); } lock.active = id; transition.event && transition.event.start.call(node, node.__data__, i); transition.tween.forEach(function(key, value) { if (value = value.call(node, node.__data__, i)) { tweened.push(value); } }); ease = transition.ease; duration = transition.duration; d3.timer(function() { timer.c = tick(elapsed || 1) ? d3_true : tick; return 1; }, 0, time); } function tick(elapsed) { if (lock.active !== id) return 1; var t = elapsed / duration, e = ease(t), n = tweened.length; while (n > 0) { tweened[--n].call(node, e); } if (t >= 1) { transition.event && transition.event.end.call(node, node.__data__, i); return stop(); } } function stop() { if (--lock.count) delete lock[id]; else delete node[ns]; return 1; } }, 0, time); } } d3.svg.axis = function() { var scale = d3.scale.linear(), orient = d3_svg_axisDefaultOrient, innerTickSize = 6, outerTickSize = 6, tickPadding = 3, tickArguments_ = [ 10 ], tickValues = null, tickFormat_; function axis(g) { g.each(function() { var g = d3.select(this); var scale0 = this.__chart__ || scale, scale1 = this.__chart__ = scale.copy(); var ticks = tickValues == null ? scale1.ticks ? scale1.ticks.apply(scale1, tickArguments_) : scale1.domain() : tickValues, tickFormat = tickFormat_ == null ? scale1.tickFormat ? scale1.tickFormat.apply(scale1, tickArguments_) : d3_identity : tickFormat_, tick = g.selectAll(".tick").data(ticks, scale1), tickEnter = tick.enter().insert("g", ".domain").attr("class", "tick").style("opacity", ε), tickExit = d3.transition(tick.exit()).style("opacity", ε).remove(), tickUpdate = d3.transition(tick.order()).style("opacity", 1), tickSpacing = Math.max(innerTickSize, 0) + tickPadding, tickTransform; var range = d3_scaleRange(scale1), path = g.selectAll(".domain").data([ 0 ]), pathUpdate = (path.enter().append("path").attr("class", "domain"), d3.transition(path)); tickEnter.append("line"); tickEnter.append("text"); var lineEnter = tickEnter.select("line"), lineUpdate = tickUpdate.select("line"), text = tick.select("text").text(tickFormat), textEnter = tickEnter.select("text"), textUpdate = tickUpdate.select("text"), sign = orient === "top" || orient === "left" ? -1 : 1, x1, x2, y1, y2; if (orient === "bottom" || orient === "top") { tickTransform = d3_svg_axisX, x1 = "x", y1 = "y", x2 = "x2", y2 = "y2"; text.attr("dy", sign < 0 ? "0em" : ".71em").style("text-anchor", "middle"); pathUpdate.attr("d", "M" + range[0] + "," + sign * outerTickSize + "V0H" + range[1] + "V" + sign * outerTickSize); } else { tickTransform = d3_svg_axisY, x1 = "y", y1 = "x", x2 = "y2", y2 = "x2"; text.attr("dy", ".32em").style("text-anchor", sign < 0 ? "end" : "start"); pathUpdate.attr("d", "M" + sign * outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + sign * outerTickSize); } lineEnter.attr(y2, sign * innerTickSize); textEnter.attr(y1, sign * tickSpacing); lineUpdate.attr(x2, 0).attr(y2, sign * innerTickSize); textUpdate.attr(x1, 0).attr(y1, sign * tickSpacing); if (scale1.rangeBand) { var x = scale1, dx = x.rangeBand() / 2; scale0 = scale1 = function(d) { return x(d) + dx; }; } else if (scale0.rangeBand) { scale0 = scale1; } else { tickExit.call(tickTransform, scale1, scale0); } tickEnter.call(tickTransform, scale0, scale1); tickUpdate.call(tickTransform, scale1, scale1); }); } axis.scale = function(x) { if (!arguments.length) return scale; scale = x; return axis; }; axis.orient = function(x) { if (!arguments.length) return orient; orient = x in d3_svg_axisOrients ? x + "" : d3_svg_axisDefaultOrient; return axis; }; axis.ticks = function() { if (!arguments.length) return tickArguments_; tickArguments_ = arguments; return axis; }; axis.tickValues = function(x) { if (!arguments.length) return tickValues; tickValues = x; return axis; }; axis.tickFormat = function(x) { if (!arguments.length) return tickFormat_; tickFormat_ = x; return axis; }; axis.tickSize = function(x) { var n = arguments.length; if (!n) return innerTickSize; innerTickSize = +x; outerTickSize = +arguments[n - 1]; return axis; }; axis.innerTickSize = function(x) { if (!arguments.length) return innerTickSize; innerTickSize = +x; return axis; }; axis.outerTickSize = function(x) { if (!arguments.length) return outerTickSize; outerTickSize = +x; return axis; }; axis.tickPadding = function(x) { if (!arguments.length) return tickPadding; tickPadding = +x; return axis; }; axis.tickSubdivide = function() { return arguments.length && axis; }; return axis; }; var d3_svg_axisDefaultOrient = "bottom", d3_svg_axisOrients = { top: 1, right: 1, bottom: 1, left: 1 }; function d3_svg_axisX(selection, x0, x1) { selection.attr("transform", function(d) { var v0 = x0(d); return "translate(" + (isFinite(v0) ? v0 : x1(d)) + ",0)"; }); } function d3_svg_axisY(selection, y0, y1) { selection.attr("transform", function(d) { var v0 = y0(d); return "translate(0," + (isFinite(v0) ? v0 : y1(d)) + ")"; }); } d3.svg.brush = function() { var event = d3_eventDispatch(brush, "brushstart", "brush", "brushend"), x = null, y = null, xExtent = [ 0, 0 ], yExtent = [ 0, 0 ], xExtentDomain, yExtentDomain, xClamp = true, yClamp = true, resizes = d3_svg_brushResizes[0]; function brush(g) { g.each(function() { var g = d3.select(this).style("pointer-events", "all").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)").on("mousedown.brush", brushstart).on("touchstart.brush", brushstart); var background = g.selectAll(".background").data([ 0 ]); background.enter().append("rect").attr("class", "background").style("visibility", "hidden").style("cursor", "crosshair"); g.selectAll(".extent").data([ 0 ]).enter().append("rect").attr("class", "extent").style("cursor", "move"); var resize = g.selectAll(".resize").data(resizes, d3_identity); resize.exit().remove(); resize.enter().append("g").attr("class", function(d) { return "resize " + d; }).style("cursor", function(d) { return d3_svg_brushCursor[d]; }).append("rect").attr("x", function(d) { return /[ew]$/.test(d) ? -3 : null; }).attr("y", function(d) { return /^[ns]/.test(d) ? -3 : null; }).attr("width", 6).attr("height", 6).style("visibility", "hidden"); resize.style("display", brush.empty() ? "none" : null); var gUpdate = d3.transition(g), backgroundUpdate = d3.transition(background), range; if (x) { range = d3_scaleRange(x); backgroundUpdate.attr("x", range[0]).attr("width", range[1] - range[0]); redrawX(gUpdate); } if (y) { range = d3_scaleRange(y); backgroundUpdate.attr("y", range[0]).attr("height", range[1] - range[0]); redrawY(gUpdate); } redraw(gUpdate); }); } brush.event = function(g) { g.each(function() { var event_ = event.of(this, arguments), extent1 = { x: xExtent, y: yExtent, i: xExtentDomain, j: yExtentDomain }, extent0 = this.__chart__ || extent1; this.__chart__ = extent1; if (d3_transitionInheritId) { d3.select(this).transition().each("start.brush", function() { xExtentDomain = extent0.i; yExtentDomain = extent0.j; xExtent = extent0.x; yExtent = extent0.y; event_({ type: "brushstart" }); }).tween("brush:brush", function() { var xi = d3_interpolateArray(xExtent, extent1.x), yi = d3_interpolateArray(yExtent, extent1.y); xExtentDomain = yExtentDomain = null; return function(t) { xExtent = extent1.x = xi(t); yExtent = extent1.y = yi(t); event_({ type: "brush", mode: "resize" }); }; }).each("end.brush", function() { xExtentDomain = extent1.i; yExtentDomain = extent1.j; event_({ type: "brush", mode: "resize" }); event_({ type: "brushend" }); }); } else { event_({ type: "brushstart" }); event_({ type: "brush", mode: "resize" }); event_({ type: "brushend" }); } }); }; function redraw(g) { g.selectAll(".resize").attr("transform", function(d) { return "translate(" + xExtent[+/e$/.test(d)] + "," + yExtent[+/^s/.test(d)] + ")"; }); } function redrawX(g) { g.select(".extent").attr("x", xExtent[0]); g.selectAll(".extent,.n>rect,.s>rect").attr("width", xExtent[1] - xExtent[0]); } function redrawY(g) { g.select(".extent").attr("y", yExtent[0]); g.selectAll(".extent,.e>rect,.w>rect").attr("height", yExtent[1] - yExtent[0]); } function brushstart() { var target = this, eventTarget = d3.select(d3.event.target), event_ = event.of(target, arguments), g = d3.select(target), resizing = eventTarget.datum(), resizingX = !/^(n|s)$/.test(resizing) && x, resizingY = !/^(e|w)$/.test(resizing) && y, dragging = eventTarget.classed("extent"), dragRestore = d3_event_dragSuppress(), center, origin = d3.mouse(target), offset; var w = d3.select(d3_window).on("keydown.brush", keydown).on("keyup.brush", keyup); if (d3.event.changedTouches) { w.on("touchmove.brush", brushmove).on("touchend.brush", brushend); } else { w.on("mousemove.brush", brushmove).on("mouseup.brush", brushend); } g.interrupt().selectAll("*").interrupt(); if (dragging) { origin[0] = xExtent[0] - origin[0]; origin[1] = yExtent[0] - origin[1]; } else if (resizing) { var ex = +/w$/.test(resizing), ey = +/^n/.test(resizing); offset = [ xExtent[1 - ex] - origin[0], yExtent[1 - ey] - origin[1] ]; origin[0] = xExtent[ex]; origin[1] = yExtent[ey]; } else if (d3.event.altKey) center = origin.slice(); g.style("pointer-events", "none").selectAll(".resize").style("display", null); d3.select("body").style("cursor", eventTarget.style("cursor")); event_({ type: "brushstart" }); brushmove(); function keydown() { if (d3.event.keyCode == 32) { if (!dragging) { center = null; origin[0] -= xExtent[1]; origin[1] -= yExtent[1]; dragging = 2; } d3_eventPreventDefault(); } } function keyup() { if (d3.event.keyCode == 32 && dragging == 2) { origin[0] += xExtent[1]; origin[1] += yExtent[1]; dragging = 0; d3_eventPreventDefault(); } } function brushmove() { var point = d3.mouse(target), moved = false; if (offset) { point[0] += offset[0]; point[1] += offset[1]; } if (!dragging) { if (d3.event.altKey) { if (!center) center = [ (xExtent[0] + xExtent[1]) / 2, (yExtent[0] + yExtent[1]) / 2 ]; origin[0] = xExtent[+(point[0] < center[0])]; origin[1] = yExtent[+(point[1] < center[1])]; } else center = null; } if (resizingX && move1(point, x, 0)) { redrawX(g); moved = true; } if (resizingY && move1(point, y, 1)) { redrawY(g); moved = true; } if (moved) { redraw(g); event_({ type: "brush", mode: dragging ? "move" : "resize" }); } } function move1(point, scale, i) { var range = d3_scaleRange(scale), r0 = range[0], r1 = range[1], position = origin[i], extent = i ? yExtent : xExtent, size = extent[1] - extent[0], min, max; if (dragging) { r0 -= position; r1 -= size + position; } min = (i ? yClamp : xClamp) ? Math.max(r0, Math.min(r1, point[i])) : point[i]; if (dragging) { max = (min += position) + size; } else { if (center) position = Math.max(r0, Math.min(r1, 2 * center[i] - min)); if (position < min) { max = min; min = position; } else { max = position; } } if (extent[0] != min || extent[1] != max) { if (i) yExtentDomain = null; else xExtentDomain = null; extent[0] = min; extent[1] = max; return true; } } function brushend() { brushmove(); g.style("pointer-events", "all").selectAll(".resize").style("display", brush.empty() ? "none" : null); d3.select("body").style("cursor", null); w.on("mousemove.brush", null).on("mouseup.brush", null).on("touchmove.brush", null).on("touchend.brush", null).on("keydown.brush", null).on("keyup.brush", null); dragRestore(); event_({ type: "brushend" }); } } brush.x = function(z) { if (!arguments.length) return x; x = z; resizes = d3_svg_brushResizes[!x << 1 | !y]; return brush; }; brush.y = function(z) { if (!arguments.length) return y; y = z; resizes = d3_svg_brushResizes[!x << 1 | !y]; return brush; }; brush.clamp = function(z) { if (!arguments.length) return x && y ? [ xClamp, yClamp ] : x ? xClamp : y ? yClamp : null; if (x && y) xClamp = !!z[0], yClamp = !!z[1]; else if (x) xClamp = !!z; else if (y) yClamp = !!z; return brush; }; brush.extent = function(z) { var x0, x1, y0, y1, t; if (!arguments.length) { if (x) { if (xExtentDomain) { x0 = xExtentDomain[0], x1 = xExtentDomain[1]; } else { x0 = xExtent[0], x1 = xExtent[1]; if (x.invert) x0 = x.invert(x0), x1 = x.invert(x1); if (x1 < x0) t = x0, x0 = x1, x1 = t; } } if (y) { if (yExtentDomain) { y0 = yExtentDomain[0], y1 = yExtentDomain[1]; } else { y0 = yExtent[0], y1 = yExtent[1]; if (y.invert) y0 = y.invert(y0), y1 = y.invert(y1); if (y1 < y0) t = y0, y0 = y1, y1 = t; } } return x && y ? [ [ x0, y0 ], [ x1, y1 ] ] : x ? [ x0, x1 ] : y && [ y0, y1 ]; } if (x) { x0 = z[0], x1 = z[1]; if (y) x0 = x0[0], x1 = x1[0]; xExtentDomain = [ x0, x1 ]; if (x.invert) x0 = x(x0), x1 = x(x1); if (x1 < x0) t = x0, x0 = x1, x1 = t; if (x0 != xExtent[0] || x1 != xExtent[1]) xExtent = [ x0, x1 ]; } if (y) { y0 = z[0], y1 = z[1]; if (x) y0 = y0[1], y1 = y1[1]; yExtentDomain = [ y0, y1 ]; if (y.invert) y0 = y(y0), y1 = y(y1); if (y1 < y0) t = y0, y0 = y1, y1 = t; if (y0 != yExtent[0] || y1 != yExtent[1]) yExtent = [ y0, y1 ]; } return brush; }; brush.clear = function() { if (!brush.empty()) { xExtent = [ 0, 0 ], yExtent = [ 0, 0 ]; xExtentDomain = yExtentDomain = null; } return brush; }; brush.empty = function() { return !!x && xExtent[0] == xExtent[1] || !!y && yExtent[0] == yExtent[1]; }; return d3.rebind(brush, event, "on"); }; var d3_svg_brushCursor = { n: "ns-resize", e: "ew-resize", s: "ns-resize", w: "ew-resize", nw: "nwse-resize", ne: "nesw-resize", se: "nwse-resize", sw: "nesw-resize" }; var d3_svg_brushResizes = [ [ "n", "e", "s", "w", "nw", "ne", "se", "sw" ], [ "e", "w" ], [ "n", "s" ], [] ]; var d3_time_format = d3_time.format = d3_locale_enUS.timeFormat; var d3_time_formatUtc = d3_time_format.utc; var d3_time_formatIso = d3_time_formatUtc("%Y-%m-%dT%H:%M:%S.%LZ"); d3_time_format.iso = Date.prototype.toISOString && +new Date("2000-01-01T00:00:00.000Z") ? d3_time_formatIsoNative : d3_time_formatIso; function d3_time_formatIsoNative(date) { return date.toISOString(); } d3_time_formatIsoNative.parse = function(string) { var date = new Date(string); return isNaN(date) ? null : date; }; d3_time_formatIsoNative.toString = d3_time_formatIso.toString; d3_time.second = d3_time_interval(function(date) { return new d3_date(Math.floor(date / 1e3) * 1e3); }, function(date, offset) { date.setTime(date.getTime() + Math.floor(offset) * 1e3); }, function(date) { return date.getSeconds(); }); d3_time.seconds = d3_time.second.range; d3_time.seconds.utc = d3_time.second.utc.range; d3_time.minute = d3_time_interval(function(date) { return new d3_date(Math.floor(date / 6e4) * 6e4); }, function(date, offset) { date.setTime(date.getTime() + Math.floor(offset) * 6e4); }, function(date) { return date.getMinutes(); }); d3_time.minutes = d3_time.minute.range; d3_time.minutes.utc = d3_time.minute.utc.range; d3_time.hour = d3_time_interval(function(date) { var timezone = date.getTimezoneOffset() / 60; return new d3_date((Math.floor(date / 36e5 - timezone) + timezone) * 36e5); }, function(date, offset) { date.setTime(date.getTime() + Math.floor(offset) * 36e5); }, function(date) { return date.getHours(); }); d3_time.hours = d3_time.hour.range; d3_time.hours.utc = d3_time.hour.utc.range; d3_time.month = d3_time_interval(function(date) { date = d3_time.day(date); date.setDate(1); return date; }, function(date, offset) { date.setMonth(date.getMonth() + offset); }, function(date) { return date.getMonth(); }); d3_time.months = d3_time.month.range; d3_time.months.utc = d3_time.month.utc.range; function d3_time_scale(linear, methods, format) { function scale(x) { return linear(x); } scale.invert = function(x) { return d3_time_scaleDate(linear.invert(x)); }; scale.domain = function(x) { if (!arguments.length) return linear.domain().map(d3_time_scaleDate); linear.domain(x); return scale; }; function tickMethod(extent, count) { var span = extent[1] - extent[0], target = span / count, i = d3.bisect(d3_time_scaleSteps, target); return i == d3_time_scaleSteps.length ? [ methods.year, d3_scale_linearTickRange(extent.map(function(d) { return d / 31536e6; }), count)[2] ] : !i ? [ d3_time_scaleMilliseconds, d3_scale_linearTickRange(extent, count)[2] ] : methods[target / d3_time_scaleSteps[i - 1] < d3_time_scaleSteps[i] / target ? i - 1 : i]; } scale.nice = function(interval, skip) { var domain = scale.domain(), extent = d3_scaleExtent(domain), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" && tickMethod(extent, interval); if (method) interval = method[0], skip = method[1]; function skipped(date) { return !isNaN(date) && !interval.range(date, d3_time_scaleDate(+date + 1), skip).length; } return scale.domain(d3_scale_nice(domain, skip > 1 ? { floor: function(date) { while (skipped(date = interval.floor(date))) date = d3_time_scaleDate(date - 1); return date; }, ceil: function(date) { while (skipped(date = interval.ceil(date))) date = d3_time_scaleDate(+date + 1); return date; } } : interval)); }; scale.ticks = function(interval, skip) { var extent = d3_scaleExtent(scale.domain()), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" ? tickMethod(extent, interval) : !interval.range && [ { range: interval }, skip ]; if (method) interval = method[0], skip = method[1]; return interval.range(extent[0], d3_time_scaleDate(+extent[1] + 1), skip < 1 ? 1 : skip); }; scale.tickFormat = function() { return format; }; scale.copy = function() { return d3_time_scale(linear.copy(), methods, format); }; return d3_scale_linearRebind(scale, linear); } function d3_time_scaleDate(t) { return new Date(t); } var d3_time_scaleSteps = [ 1e3, 5e3, 15e3, 3e4, 6e4, 3e5, 9e5, 18e5, 36e5, 108e5, 216e5, 432e5, 864e5, 1728e5, 6048e5, 2592e6, 7776e6, 31536e6 ]; var d3_time_scaleLocalMethods = [ [ d3_time.second, 1 ], [ d3_time.second, 5 ], [ d3_time.second, 15 ], [ d3_time.second, 30 ], [ d3_time.minute, 1 ], [ d3_time.minute, 5 ], [ d3_time.minute, 15 ], [ d3_time.minute, 30 ], [ d3_time.hour, 1 ], [ d3_time.hour, 3 ], [ d3_time.hour, 6 ], [ d3_time.hour, 12 ], [ d3_time.day, 1 ], [ d3_time.day, 2 ], [ d3_time.week, 1 ], [ d3_time.month, 1 ], [ d3_time.month, 3 ], [ d3_time.year, 1 ] ]; var d3_time_scaleLocalFormat = d3_time_format.multi([ [ ".%L", function(d) { return d.getMilliseconds(); } ], [ ":%S", function(d) { return d.getSeconds(); } ], [ "%I:%M", function(d) { return d.getMinutes(); } ], [ "%I %p", function(d) { return d.getHours(); } ], [ "%a %d", function(d) { return d.getDay() && d.getDate() != 1; } ], [ "%b %d", function(d) { return d.getDate() != 1; } ], [ "%B", function(d) { return d.getMonth(); } ], [ "%Y", d3_true ] ]); var d3_time_scaleMilliseconds = { range: function(start, stop, step) { return d3.range(Math.ceil(start / step) * step, +stop, step).map(d3_time_scaleDate); }, floor: d3_identity, ceil: d3_identity }; d3_time_scaleLocalMethods.year = d3_time.year; d3_time.scale = function() { return d3_time_scale(d3.scale.linear(), d3_time_scaleLocalMethods, d3_time_scaleLocalFormat); }; var d3_time_scaleUtcMethods = d3_time_scaleLocalMethods.map(function(m) { return [ m[0].utc, m[1] ]; }); var d3_time_scaleUtcFormat = d3_time_formatUtc.multi([ [ ".%L", function(d) { return d.getUTCMilliseconds(); } ], [ ":%S", function(d) { return d.getUTCSeconds(); } ], [ "%I:%M", function(d) { return d.getUTCMinutes(); } ], [ "%I %p", function(d) { return d.getUTCHours(); } ], [ "%a %d", function(d) { return d.getUTCDay() && d.getUTCDate() != 1; } ], [ "%b %d", function(d) { return d.getUTCDate() != 1; } ], [ "%B", function(d) { return d.getUTCMonth(); } ], [ "%Y", d3_true ] ]); d3_time_scaleUtcMethods.year = d3_time.year.utc; d3_time.scale.utc = function() { return d3_time_scale(d3.scale.linear(), d3_time_scaleUtcMethods, d3_time_scaleUtcFormat); }; d3.text = d3_xhrType(function(request) { return request.responseText; }); d3.json = function(url, callback) { return d3_xhr(url, "application/json", d3_json, callback); }; function d3_json(request) { return JSON.parse(request.responseText); } d3.html = function(url, callback) { return d3_xhr(url, "text/html", d3_html, callback); }; function d3_html(request) { var range = d3_document.createRange(); range.selectNode(d3_document.body); return range.createContextualFragment(request.responseText); } d3.xml = d3_xhrType(function(request) { return request.responseXML; }); if (typeof define === "function" && define.amd) define(d3); else if (typeof module === "object" && module.exports) module.exports = d3; this.d3 = d3; }();; /*global d3*/ H5P.Chart.PieChart = (function () { /** * Creates a pie chart from the given data set. * * @class * @param {array} params from semantics, contains data set * @param {H5P.jQuery} $wrapper */ function PieChart(params, $wrapper) { var self = this; var dataSet = params.listOfTypes; var defColors = d3.scale.ordinal() .range(["#90EE90", "#ADD8E6", "#FFB6C1", "#B0C4DE", "#D3D3D3", "#20B2AA", "#FAFAD2"]); // Create SVG var svg = d3.select($wrapper[0]) .append("svg"); svg.append("desc").html(params.figureDefinition); var translater = svg.append("g") .attr("class", "translater"); var pie = d3.layout.pie() .sort(null) .value(function(d) { return d.value; }); var arcs = translater.selectAll(".arc") .data(pie(dataSet)) .enter().append("g") .attr("class", "arc"); var paths = arcs.append("path") .style("fill", function(d) { if (d.data.color !== undefined) { return d.data.color; } return defColors(dataSet.indexOf(d.data) % 7); }); var texts = arcs.append("svg:text") .attr("class", "text") .attr("aria-hidden", true) .attr("text-anchor", "middle") .text(function(d, i) { return dataSet[i].text + ': ' + dataSet[i].value; }) .attr("fill", function (d) { if (d.data.fontColor !== undefined) { return d.data.fontColor; } }); /** * Fit the current chart to the size of the wrapper. */ self.resize = function () { // Scale to smallest value of height and width var style = window.getComputedStyle($wrapper[0]); var scaleTo = Math.min(parseFloat(style.width), parseFloat(style.height)); // Do the math var width = scaleTo; var height = scaleTo; var padding = 0; var radius = Math.min(width, height) / 2; var arc = d3.svg.arc() .outerRadius(radius - padding) .innerRadius(0); // Update positions svg.attr('width', width + 'px') .attr('height', height + 'px'); translater.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")"); paths.attr("d", arc); texts.attr("transform", function(d) { d.innerRadius = 0; d.outerRadius = radius - padding; return "translate(" + arc.centroid(d) + ")"; }); }; } return PieChart; })(); ; /*global d3*/ H5P.Chart.BarChart = (function () { /** * Creates a bar chart from the given data set. * * @class * @param {array} params from semantics, contains data set * @param {H5P.jQuery} $wrapper */ function BarChart(params, $wrapper) { var self = this; var dataSet = params.listOfTypes; var defColors = d3.scale.ordinal() .range(["#90EE90", "#ADD8E6", "#FFB6C1", "#B0C4DE", "#D3D3D3", "#20B2AA", "#FAFAD2"]); // Create scales for bars var xScale = d3.scale.ordinal() .domain(d3.range(dataSet.length)); var yScale = d3.scale.linear() .domain([0, d3.max(dataSet, function (d) { return d.value; })]); var x = d3.time.scale(); var y = d3.scale.linear(); var xAxis = d3.svg.axis() .scale(xScale) .orient("bottom") .tickFormat(function (d) { return dataSet[d % dataSet.length].text; }); // Create SVG element var svg = d3.select($wrapper[0]) .append("svg"); svg.append("desc").html("chart"); // Create x axis var xAxisG = svg.append("g") .attr("class", "x-axis"); /** * @private */ var key = function (d) { return dataSet.indexOf(d); }; // Create rectangles for bars var rects = svg.selectAll("rect") .data(dataSet, key) .enter() .append("rect") .attr("fill", function(d) { if (d.color !== undefined) { return d.color; } return defColors(dataSet.indexOf(d) % 7); }); // Create labels var texts = svg.selectAll("text") .data(dataSet, key) .enter() .append("text") .text(function(d) { return d.value; }) .attr("text-anchor", "middle") .attr("fill", function (d) { if (d.fontColor !== undefined) { return d.fontColor; } return '000000'; }) .attr("aria-hidden", true); /** * Fit the current bar chart to the size of the wrapper. */ self.resize = function () { // Always scale to available space var style = window.getComputedStyle($wrapper[0]); var width = parseFloat(style.width); var h = parseFloat(style.height); var fontSize = parseFloat(style.fontSize); var lineHeight = (1.25 * fontSize); var tickSize = (fontSize * 0.125); var height = h - tickSize - lineHeight; // Add space for labels below // Update SVG size svg.attr("width", width) .attr("height", h); // Update scales xScale.rangeRoundBands([0, width], 0.05); yScale.range([0, height]); x.range([0, width]); y.range([height, 0]); xAxis.tickSize([tickSize]); xAxisG.attr("transform", "translate(0," + height + ")") .call(xAxis); // Move rectangles (bars) rects.attr("x", function(d, i) { return xScale(i); }).attr("y", function(d) { return height - yScale(d.value); }).attr("width", xScale.rangeBand()) .attr("height", function(d) { return yScale(d.value); }); // Re-locate text value labels texts.attr("x", function(d, i) { return xScale(i) + xScale.rangeBand() / 2; }).attr("y", function(d) { return height - yScale(d.value) + lineHeight; }); // Hide ticks from readspeakers, the entire rectangle is already labelled xAxisG.selectAll("text").attr("aria-hidden", true); }; } return BarChart; })(); ; H5P.Column = (function (EventDispatcher) { /** * Column Constructor * * @class * @param {Object} params Describes task behavior * @param {number} id Content identifier * @param {Object} data User specific data to adapt behavior */ function Column(params, id, data) { /** @alias H5P.Column# */ var self = this; // We support events by extending this class EventDispatcher.call(self); // Add defaults params = params || {}; if (params.useSeparators === undefined) { params.useSeparators = true; } this.contentData = data; // Column wrapper element var wrapper; // H5P content in the column var instances = []; var instanceContainers = []; // Number of tasks among instances var numTasks = 0; // Number of tasks that has been completed var numTasksCompleted = 0; // Keep track of result for each task var tasksResultEvent = []; // Keep track of last content's margin state var previousHasMargin; /** * Calculate score and trigger completed event. * * @private */ var completed = function () { // Sum all scores var raw = 0; var max = 0; for (var i = 0; i < tasksResultEvent.length; i++) { var event = tasksResultEvent[i]; raw += event.getScore(); max += event.getMaxScore(); } self.triggerXAPIScored(raw, max, 'completed'); }; /** * Generates an event handler for the given task index. * * @private * @param {number} taskIndex * @return {function} xAPI event handler */ var trackScoring = function (taskIndex) { return function (event) { if (event.getScore() === null) { return; // Skip, not relevant } if (tasksResultEvent[taskIndex] === undefined) { // Update number of completed tasks numTasksCompleted++; } // Keep track of latest event with result tasksResultEvent[taskIndex] = event; // Track progress var progressed = self.createXAPIEventTemplate('progressed'); progressed.data.statement.object.definition.extensions['http://id.tincanapi.com/extension/ending-point'] = taskIndex + 1; self.trigger(progressed); // Check to see if we're done if (numTasksCompleted === numTasks) { // Run this after the current event is sent setTimeout(function () { completed(); // Done }, 0); } }; }; /** * Creates a new ontent instance from the given content parameters and * then attaches it the wrapper. Sets up event listeners. * * @private * @param {Object} content Parameters * @param {Object} [contentData] Content Data */ var addRunnable = function (content, contentData) { // Create container for content var container = document.createElement('div'); container.classList.add('h5p-column-content'); // Content overrides var library = content.library.split(' ')[0]; if (library === 'H5P.Video') { // Prevent video from growing endlessly since height is unlimited. content.params.visuals.fit = false; } // Create content instance var instance = H5P.newRunnable(content, id, undefined, true, contentData); // Bubble resize events bubbleUp(instance, 'resize', self); // Check if instance is a task if (Column.isTask(instance)) { // Tasks requires completion instance.on('xAPI', trackScoring(numTasks)); numTasks++; } if (library === 'H5P.Image') { // Resize when images are loaded instance.on('loaded', function () { self.trigger('resize'); }); } // Keep track of all instances instances.push(instance); instanceContainers.push({ hasAttached: false, container: container, instanceIndex: instances.length - 1, }); // Add to DOM wrapper wrapper.appendChild(container); }; /** * Help get data for content at given index * * @private * @param {number} index * @returns {Object} Data object with previous state */ var grabContentData = function (index) { var contentData = { parent: self }; if (data.previousState && data.previousState.instances && data.previousState.instances[index]) { contentData.previousState = data.previousState.instances[index]; } return contentData; }; /** * Adds separator before the next content. * * @private * @param {string} libraryName Name of the next content type * @param {string} useSeparator */ var addSeparator = function (libraryName, useSeparator) { // Determine separator spacing var thisHasMargin = (hasMargins.indexOf(libraryName) !== -1); // Only add if previous content exists if (previousHasMargin !== undefined) { // Create separator element var separator = document.createElement('div'); //separator.classList.add('h5p-column-ruler'); // If no margins, check for top margin only if (!thisHasMargin && (hasTopMargins.indexOf(libraryName) === -1)) { if (!previousHasMargin) { // None of them have margin // Only add separator if forced if (useSeparator === 'enabled') { // Add ruler separator.classList.add('h5p-column-ruler'); // Add space both before and after the ruler separator.classList.add('h5p-column-space-before-n-after'); } else { // Default is to separte using a single space, no ruler separator.classList.add('h5p-column-space-before'); } } else { // We don't have any margin but the previous content does // Only add separator if forced if (useSeparator === 'enabled') { // Add ruler separator.classList.add('h5p-column-ruler'); // Add space after the ruler separator.classList.add('h5p-column-space-after'); } } } else if (!previousHasMargin) { // We have margin but not the previous content doesn't // Only add separator if forced if (useSeparator === 'enabled') { // Add ruler separator.classList.add('h5p-column-ruler'); // Add space after the ruler separator.classList.add('h5p-column-space-before'); } } else { // Both already have margin if (useSeparator !== 'disabled') { // Default is to add ruler unless its disabled separator.classList.add('h5p-column-ruler'); } } // Insert into DOM wrapper.appendChild(separator); } // Keep track of spacing for next separator previousHasMargin = thisHasMargin || (hasBottomMargins.indexOf(libraryName) !== -1); }; /** * Creates a wrapper and the column content the first time the column * is attached to the DOM. * * @private */ var createHTML = function () { // Create wrapper wrapper = document.createElement('div'); // Go though all contents for (var i = 0; i < params.content.length; i++) { var content = params.content[i]; // In case the author has created an element without selecting any // library if (content.content === undefined) { continue; } if (params.useSeparators) { // (check for global override) // Add separator between contents addSeparator(content.content.library.split(' ')[0], content.useSeparator); } // Add content addRunnable(content.content, grabContentData(i)); } }; /** * Attach the column to the given container * * @param {H5P.jQuery} $container */ self.attach = function ($container) { if (wrapper === undefined) { // Create wrapper and content createHTML(); } // Attach instances that have not been attached instanceContainers.filter(function (container) { return !container.hasAttached }) .forEach(function (container) { instances[container.instanceIndex] .attach(H5P.jQuery(container.container)); // Remove any fullscreen buttons disableFullscreen(instances[container.instanceIndex]); }); // Add to DOM $container.addClass('h5p-column').html('').append(wrapper); }; /** * Create object containing information about the current state * of this content. * * @return {Object} */ self.getCurrentState = function () { // Get previous state object or create new state object var state = (data.previousState ? data.previousState : {}); if (!state.instances) { state.instances = []; } // Grab the current state for each instance for (var i = 0; i < instances.length; i++) { var instance = instances[i]; if (instance.getCurrentState instanceof Function || typeof instance.getCurrentState === 'function') { state.instances[i] = instance.getCurrentState(); } } // Done return state; }; /** * Get xAPI data. * Contract used by report rendering engine. * * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6} */ self.getXAPIData = function () { var xAPIEvent = self.createXAPIEventTemplate('answered'); addQuestionToXAPI(xAPIEvent); xAPIEvent.setScoredResult(self.getScore(), self.getMaxScore(), self, true, self.getScore() === self.getMaxScore() ); return { statement: xAPIEvent.data.statement, children: getXAPIDataFromChildren(instances) }; }; /** * Get score for all children * Contract used for getting the complete score of task. * * @return {number} Score for questions */ self.getScore = function () { return instances.reduce(function (prev, instance) { return prev + (instance.getScore ? instance.getScore() : 0); }, 0); }; /** * Get maximum score possible for all children instances * Contract. * * @return {number} Maximum score for questions */ self.getMaxScore = function () { return instances.reduce(function (prev, instance) { return prev + (instance.getMaxScore ? instance.getMaxScore() : 0); }, 0); }; /** * Get answer given * Contract. * * @return {boolean} True, if all answers have been given. */ self.getAnswerGiven = function () { return instances.reduce(function (prev, instance) { return prev && (instance.getAnswerGiven ? instance.getAnswerGiven() : prev); }, true); }; /** * Show solutions. * Contract. */ self.showSolutions = function () { instances.forEach(function (instance) { if (instance.toggleReadSpeaker) { instance.toggleReadSpeaker(true); } if (instance.showSolutions) { instance.showSolutions(); } if (instance.toggleReadSpeaker) { instance.toggleReadSpeaker(false); } }); }; /** * Reset task. * Contract. */ self.resetTask = function () { instances.forEach(function (instance) { if (instance.resetTask) { instance.resetTask(); } }); }; /** * Get instances for all children * TODO: This is not a good interface, we should provide handling needed * handling of the tasks instead of repeating them for each parent... * * @return {Object[]} array of instances */ self.getInstances = function () { return instances; }; /** * Get title, e.g. for xAPI when Column is subcontent. * * @return {string} Title. */ self.getTitle = function () { return H5P.createTitle((self.contentData && self.contentData.metadata && self.contentData.metadata.title) ? self.contentData.metadata.title : 'Column'); }; /** * Add the question itself to the definition part of an xAPIEvent */ var addQuestionToXAPI = function (xAPIEvent) { var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']); H5P.jQuery.extend(definition, getxAPIDefinition()); }; /** * Generate xAPI object definition used in xAPI statements. * @return {Object} */ var getxAPIDefinition = function () { var definition = {}; definition.interactionType = 'compound'; definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction'; definition.description = { 'en-US': '' }; return definition; }; /** * Get xAPI data from sub content types * * @param {Array} of H5P instances * @returns {Array} of xAPI data objects used to build a report */ var getXAPIDataFromChildren = function (children) { return children.map(function (child) { if (typeof child.getXAPIData == 'function') { return child.getXAPIData(); } }).filter(function (data) { return !!data; }); }; // Resize children to fit inside parent bubbleDown(self, 'resize', instances); if (wrapper === undefined) { // Create wrapper and content createHTML(); } self.setActivityStarted(); } Column.prototype = Object.create(EventDispatcher.prototype); Column.prototype.constructor = Column; /** * Makes it easy to bubble events from parent to children * * @private * @param {Object} origin Origin of the Event * @param {string} eventName Name of the Event * @param {Array} targets Targets to trigger event on */ function bubbleDown(origin, eventName, targets) { origin.on(eventName, function (event) { if (origin.bubblingUpwards) { return; // Prevent send event back down. } for (var i = 0; i < targets.length; i++) { targets[i].trigger(eventName, event); } }); } /** * Makes it easy to bubble events from child to parent * * @private * @param {Object} origin Origin of the Event * @param {string} eventName Name of the Event * @param {Object} target Target to trigger event on */ function bubbleUp(origin, eventName, target) { origin.on(eventName, function (event) { // Prevent target from sending event back down target.bubblingUpwards = true; // Trigger event target.trigger(eventName, event); // Reset target.bubblingUpwards = false; }); } /** * Definition of which content types are tasks */ var isTasks = [ 'H5P.ImageHotspotQuestion', 'H5P.Blanks', 'H5P.Essay', 'H5P.SingleChoiceSet', 'H5P.MultiChoice', 'H5P.TrueFalse', 'H5P.DragQuestion', 'H5P.Summary', 'H5P.DragText', 'H5P.MarkTheWords', 'H5P.MemoryGame', 'H5P.QuestionSet', 'H5P.InteractiveVideo', 'H5P.CoursePresentation', 'H5P.DocumentationTool', 'H5P.MultiMediaChoice' ]; /** * Check if the given content instance is a task (will give a score) * * @param {Object} instance * @return {boolean} */ Column.isTask = function (instance) { if (instance.isTask !== undefined) { return instance.isTask; // Content will determine self if it's a task } // Go through the valid task names for (var i = 0; i < isTasks.length; i++) { // Check against library info. (instanceof is broken in H5P.newRunnable) if (instance.libraryInfo.machineName === isTasks[i]) { return true; } } return false; } /** * Definition of which content type have margins */ var hasMargins = [ 'H5P.AdvancedText', 'H5P.AudioRecorder', 'H5P.Essay', 'H5P.Link', 'H5P.Accordion', 'H5P.Table', 'H5P.GuessTheAnswer', 'H5P.Blanks', 'H5P.MultiChoice', 'H5P.TrueFalse', 'H5P.DragQuestion', 'H5P.Summary', 'H5P.DragText', 'H5P.MarkTheWords', 'H5P.ImageHotspotQuestion', 'H5P.MemoryGame', 'H5P.Dialogcards', 'H5P.QuestionSet', 'H5P.DocumentationTool' ]; /** * Definition of which content type have top margins */ var hasTopMargins = [ 'H5P.SingleChoiceSet' ]; /** * Definition of which content type have bottom margins */ var hasBottomMargins = [ 'H5P.CoursePresentation', 'H5P.Dialogcards', 'H5P.GuessTheAnswer', 'H5P.ImageSlider' ]; /** * Remove custom fullscreen buttons from sub content. * (A bit of a hack, there should have been some sort of override…) * * @param {Object} instance */ function disableFullscreen(instance) { switch (instance.libraryInfo.machineName) { case 'H5P.CoursePresentation': if (instance.$fullScreenButton) { instance.$fullScreenButton.remove(); } break; case 'H5P.InteractiveVideo': instance.on('controls', function () { if (instance.controls.$fullscreen) { instance.controls.$fullscreen.remove(); } }); break; } } return Column; })(H5P.EventDispatcher); ;