text:
/*****************************************************************
 ** Author: Asvin Goel, goel@telematique.eu
 **
 ** A plugin for reveal.js adding a chalkboard.
 **
 ** Version: 2.1.0
 **
 ** License: MIT license (see LICENSE.md)
 **
 ** Credits:
 ** Chalkboard effect by Mohamed Moustafa https://github.com/mmoustafa/Chalkboard
 ** Multi color support initially added by Kurt Rinnert https://github.com/rinnert
 ** Compatibility with reveal.js v4 by Hakim El Hattab https://github.com/hakimel
 ******************************************************************/

window.RevealChalkboard = window.RevealChalkboard || {
	id: 'RevealChalkboard',
	init: function (deck) {
		initChalkboard(deck);
	},
	configure: function (config) {
		configure(config);
	},
	toggleNotesCanvas: function () {
		toggleNotesCanvas();
	},
	toggleChalkboard: function () {
		toggleChalkboard();
	},
	colorIndex: function () {
		colorIndex();
	},
	colorNext: function () {
		colorNext();
	},
	colorPrev: function () {
		colorPrev();
	},
	clear: function () {
		clear();
	},
	reset: function () {
		reset();
	},
	resetAll: function () {
		resetAll();
	},
	updateStorage: function () {
		updateStorage();
	},
	getData: function () {
		return getData();
	},
	download: function () {
		download();
	},
};

function scriptPath() {
	// obtain plugin path from the script element
	var src;
	if (document.currentScript) {
		src = document.currentScript.src;
	} else {
		var sel = document.querySelector('script[src$="/chalkboard/plugin.js"]')
		if (sel) {
			src = sel.src;
		}
	}
	var path = (src === undefined) ? "" : src.slice(0, src.lastIndexOf("/") + 1);
	//console.log("Path: " + path);
	return path;
}
var path = scriptPath();

const initChalkboard = function (Reveal) {
	//console.warn(path);
	/* Feature detection for passive event handling*/
	var passiveSupported = false;

	try {
		window.addEventListener('test', null, Object.defineProperty({}, 'passive', {
			get: function () {
				passiveSupported = true;
			}
		}));
	} catch (err) { }

	/*****************************************************************
	 ** Configuration
	 ******************************************************************/
	var background, pen, draw, color;
	var grid = false;
	var boardmarkerWidth = 3;
	var chalkWidth = 7;
	var chalkEffect = 1.0;
	var rememberColor = [true, false];
	var eraser = {
		src: path + 'img/sponge.png',
		radius: 20
	};
	var boardmarkers = [{
		color: 'rgba(100,100,100,1)',
		cursor: 'url(' + path + 'img/boardmarker-black.png), auto'
	},
	{
		color: 'rgba(30,144,255, 1)',
		cursor: 'url(' + path + 'img/boardmarker-blue.png), auto'
	},
	{
		color: 'rgba(220,20,60,1)',
		cursor: 'url(' + path + 'img/boardmarker-red.png), auto'
	},
	{
		color: 'rgba(50,205,50,1)',
		cursor: 'url(' + path + 'img/boardmarker-green.png), auto'
	},
	{
		color: 'rgba(255,140,0,1)',
		cursor: 'url(' + path + 'img/boardmarker-orange.png), auto'
	},
	{
		color: 'rgba(150,0,20150,1)',
		cursor: 'url(' + path + 'img/boardmarker-purple.png), auto'
	},
	{
		color: 'rgba(255,220,0,1)',
		cursor: 'url(' + path + 'img/boardmarker-yellow.png), auto'
	}
	];
	var chalks = [{
		color: 'rgba(255,255,255,0.5)',
		cursor: 'url(' + path + 'img/chalk-white.png), auto'
	},
	{
		color: 'rgba(96, 154, 244, 0.5)',
		cursor: 'url(' + path + 'img/chalk-blue.png), auto'
	},
	{
		color: 'rgba(237, 20, 28, 0.5)',
		cursor: 'url(' + path + 'img/chalk-red.png), auto'
	},
	{
		color: 'rgba(20, 237, 28, 0.5)',
		cursor: 'url(' + path + 'img/chalk-green.png), auto'
	},
	{
		color: 'rgba(220, 133, 41, 0.5)',
		cursor: 'url(' + path + 'img/chalk-orange.png), auto'
	},
	{
		color: 'rgba(220,0,220,0.5)',
		cursor: 'url(' + path + 'img/chalk-purple.png), auto'
	},
	{
		color: 'rgba(255,220,0,0.5)',
		cursor: 'url(' + path + 'img/chalk-yellow.png), auto'
	}
	];
	var keyBindings = {
		toggleNotesCanvas: {
			keyCode: 67,
			key: 'C',
			description: 'Toggle notes canvas'
		},
		toggleChalkboard: {
			keyCode: 66,
			key: 'B',
			description: 'Toggle chalkboard'
		},
		clear: {
			keyCode: 46,
			key: 'DEL',
			description: 'Clear drawings on slide'
		},
		/*
				reset: {
					keyCode: 173,
					key: '-',
					description: 'Reset drawings on slide'
				},
		*/
		resetAll: {
			keyCode: 8,
			key: 'BACKSPACE',
			description: 'Reset all drawings'
		},
		colorNext: {
			keyCode: 88,
			key: 'X',
			description: 'Next color'
		},
		colorPrev: {
			keyCode: 89,
			key: 'Y',
			description: 'Previous color'
		},
		download: {
			keyCode: 68,
			key: 'D',
			description: 'Download drawings'
		}
	};

	var theme = 'chalkboard';
	var color = [0, 0];
	var toggleChalkboardButton = false;
	var toggleNotesButton = false;
	var colorButtons = true;
	var boardHandle = true;
	var transition = 800;

	var readOnly = false;
	var messageType = 'broadcast';

	var config = configure(Reveal.getConfig().chalkboard || {});
	if (config.keyBindings) {
		for (var key in config.keyBindings) {
			keyBindings[key] = config.keyBindings[key];
		};
	}

	function configure(config) {

		if (config.boardmarkerWidth || config.penWidth) boardmarkerWidth = config.boardmarkerWidth || config.penWidth;
		if (config.chalkWidth) chalkWidth = config.chalkWidth;
		if (config.chalkEffect) chalkEffect = config.chalkEffect;
		if (config.rememberColor) rememberColor = config.rememberColor;
		if (config.eraser) eraser = config.eraser;
		if (config.boardmarkers) boardmarkers = config.boardmarkers;
		if (config.chalks) chalks = config.chalks;

		if (config.theme) theme = config.theme;
		switch (theme) {
			case 'whiteboard':
				background = ['rgba(127,127,127,.1)', path + 'img/whiteboard.png'];
				draw = [drawWithBoardmarker, drawWithBoardmarker];
				pens = [boardmarkers, boardmarkers];
				grid = {
					color: 'rgb(127,127,255,0.1)',
					distance: 40,
					width: 2
				};
				break;
			case 'chalkboard':
			default:
				background = ['rgba(127,127,127,.1)', path + 'img/blackboard.png'];
				draw = [drawWithBoardmarker, drawWithChalk];
				pens = [boardmarkers, chalks];
				grid = {
					color: 'rgb(50,50,10,0.5)',
					distance: 80,
					width: 2
				};
		}

		if (config.background) background = config.background;
		if (config.grid != undefined) grid = config.grid;

		if (config.toggleChalkboardButton != undefined) toggleChalkboardButton = config.toggleChalkboardButton;
		if (config.toggleNotesButton != undefined) toggleNotesButton = config.toggleNotesButton;
		if (config.colorButtons != undefined) colorButtons = config.colorButtons;
		if (config.boardHandle != undefined) boardHandle = config.boardHandle;
		if (config.transition) transition = config.transition;

		if (config.readOnly != undefined) readOnly = config.readOnly;
		if (config.messageType) messageType = config.messageType;

		if (drawingCanvas && (config.theme || config.background || config.grid)) {
			var canvas = document.getElementById(drawingCanvas[1].id);
			canvas.style.background = 'url("' + background[1] + '") repeat';
			clearCanvas(1);
			drawGrid();
		}

		return config;
	}
	/*****************************************************************
	 ** Setup
	 ******************************************************************/

	function whenReady(callback) {
		// wait for markdown to be parsed and code to be highlighted
		if (!document.querySelector('section[data-markdown]:not([data-markdown-parsed])')
			&& !document.querySelector('code[data-line-numbers*="|"]')
		) {
			callback();
		} else {
			console.log("Wait for markdown to be parsed and code to be highlighted");
			setTimeout(whenReady, 500, callback)
		}
	}

	function whenLoaded(callback) {
		// wait for drawings to be loaded and markdown to be parsed
		if (loaded !== null) {
			callback();
		} else {
			console.log("Wait for drawings to be loaded");
			setTimeout(whenLoaded, 500, callback)
		}
	}

	if (toggleChalkboardButton) {
		console.warn("toggleChalkboardButton is deprecated, use customcontrols plugin instead!");
		//console.log("toggleChalkboardButton")
		var button = document.createElement('div');
		button.className = "chalkboard-button";
		button.id = "toggle-chalkboard";
		button.style.visibility = "visible";
		button.style.position = "absolute";
		button.style.zIndex = 30;
		button.style.fontSize = "24px";

		button.style.left = toggleChalkboardButton.left || "30px";
		button.style.bottom = toggleChalkboardButton.bottom || "30px";
		button.style.top = toggleChalkboardButton.top || "auto";
		button.style.right = toggleChalkboardButton.right || "auto";

		button.innerHTML = ''
		document.querySelector(".reveal").appendChild(button);
	}
	if (toggleNotesButton) {
		console.warn("toggleNotesButton is deprecated, use customcontrols plugin instead!");
		//console.log("toggleNotesButton")
		var button = document.createElement('div');
		button.className = "chalkboard-button";
		button.id = "toggle-notes";
		button.style.position = "absolute";
		button.style.zIndex = 30;
		button.style.fontSize = "24px";

		button.style.left = toggleNotesButton.left || "70px";
		button.style.bottom = toggleNotesButton.bottom || "30px";
		button.style.top = toggleNotesButton.top || "auto";
		button.style.right = toggleNotesButton.right || "auto";

		button.innerHTML = ''
		document.querySelector(".reveal").appendChild(button);
	}

	var drawingCanvas = [{
		id: 'notescanvas'
	}, {
		id: 'chalkboard'
	}];
	setupDrawingCanvas(0);
	setupDrawingCanvas(1);

	var mode = 0; // 0: notes canvas, 1: chalkboard
	var board = 0; // board index (only for chalkboard)

	var mouseX = 0;
	var mouseY = 0;
	var lastX = null;
	var lastY = null;

	var drawing = false;
	var erasing = false;

	var slideStart = Date.now();
	var slideIndices = {
		h: 0,
		v: 0
	};

	var timeouts = [
		[],
		[]
	];
	var touchTimeout = null;
	var slidechangeTimeout = null;
	var updateStorageTimeout = null;
	var playback = false;

	function createPalette(colors, length) {
		if (length === true || length > colors.length) {
			length = colors.length;
		}
		var palette = document.createElement('div');
		palette.classList.add('palette');
		var list = document.createElement('ul');
		// color pickers
		for (var i = 0; i < length; i++) {
			var colorButton = document.createElement('li');
			colorButton.setAttribute('data-color', i);
			colorButton.innerHTML = '';
			colorButton.style.color = colors[i].color;
			colorButton.addEventListener('click', function (e) {
				var element = e.target;
				while (!element.hasAttribute('data-color')) {
					element = element.parentElement;
				}
				colorIndex(parseInt(element.getAttribute('data-color')));
			});
			colorButton.addEventListener('touchstart', function (e) {
				var element = e.target;
				while (!element.hasAttribute('data-color')) {
					element = element.parentElement;
				}
				colorIndex(parseInt(element.getAttribute('data-color')));
			});
			list.appendChild(colorButton);
		}
		palette.appendChild(list);
		return palette;
	};

	function switchBoard(boardIdx) {
		selectBoard(boardIdx, true);
		// broadcast
		var message = new CustomEvent(messageType);
		message.content = {
			sender: 'chalkboard-plugin',
			type: 'selectboard',
			timestamp: Date.now() - slideStart,
			mode,
			board
		};
		document.dispatchEvent(message);
	}

	function setupDrawingCanvas(id) {
		var container = document.createElement('div');
		container.id = drawingCanvas[id].id;
		container.classList.add('overlay');
		container.setAttribute('data-prevent-swipe', 'true');
		container.oncontextmenu = function () {
			return false;
		}
		container.style.cursor = pens[id][color[id]].cursor;

		drawingCanvas[id].width = window.innerWidth;
		drawingCanvas[id].height = window.innerHeight;
		drawingCanvas[id].scale = 1;
		drawingCanvas[id].xOffset = 0;
		drawingCanvas[id].yOffset = 0;

		if (id == "0") {
			container.style.background = 'rgba(0,0,0,0)';
			container.style.zIndex = 24;
			container.style.opacity = 1;
			container.style.visibility = 'visible';
			container.style.pointerEvents = 'none';

			var slides = document.querySelector('.slides');
			var aspectRatio = Reveal.getConfig().width / Reveal.getConfig().height;
			if (drawingCanvas[id].width > drawingCanvas[id].height * aspectRatio) {
				drawingCanvas[id].xOffset = (drawingCanvas[id].width - drawingCanvas[id].height * aspectRatio) / 2;
			} else if (drawingCanvas[id].height > drawingCanvas[id].width / aspectRatio) {
				drawingCanvas[id].yOffset = (drawingCanvas[id].height - drawingCanvas[id].width / aspectRatio) / 2;
			}

			if (colorButtons) {
				var palette = createPalette(boardmarkers, colorButtons);
				palette.style.visibility = 'hidden'; // only show palette in drawing mode
				container.appendChild(palette);
			}
		} else {
			container.style.background = 'url("' + background[id] + '") repeat';
			container.style.zIndex = 26;
			container.style.opacity = 0;
			container.style.visibility = 'hidden';

			if (colorButtons) {
				var palette = createPalette(chalks, colorButtons);
				container.appendChild(palette);
			}
			if (boardHandle) {
				var handle = document.createElement('div');
				handle.classList.add('boardhandle');
				handle.innerHTML = '

';
				handle.querySelector('#previousboard').addEventListener('click', function (e) {
					e.preventDefault();
					switchBoard(board - 1);
				});
				handle.querySelector('#nextboard').addEventListener('click', function (e) {
					e.preventDefault();
					switchBoard(board + 1);
				});
				handle.querySelector('#previousboard').addEventListener('touchstart', function (e) {
					e.preventDefault();
					switchBoard(board - 1);
				});
				handle.querySelector('#nextboard').addEventListener('touchstart', function (e) {
					e.preventDefault();
					switchBoard(board + 1);
				});

				container.appendChild(handle);
			}
		}

		var sponge = document.createElement('img');
		sponge.src = eraser.src;
		sponge.id = 'sponge';
		sponge.style.visibility = 'hidden';
		sponge.style.position = 'absolute';
		container.appendChild(sponge);
		drawingCanvas[id].sponge = sponge;

		var canvas = document.createElement('canvas');
		canvas.width = drawingCanvas[id].width;
		canvas.height = drawingCanvas[id].height;
		canvas.setAttribute('data-chalkboard', id);
		canvas.style.cursor = pens[id][color[id]].cursor;
		container.appendChild(canvas);
		drawingCanvas[id].canvas = canvas;

		drawingCanvas[id].context = canvas.getContext('2d');

		setupCanvasEvents(container);

		document.querySelector('.reveal').appendChild(container);
		drawingCanvas[id].container = container;
	}

	/*****************************************************************
	 ** Storage
	 ******************************************************************/

	var storage = [{
		width: Reveal.getConfig().width,
		height: Reveal.getConfig().height,
		data: []
	},
	{
		width: Reveal.getConfig().width,
		height: Reveal.getConfig().height,
		data: []
	}
	];

	var loaded = null;

	if (config.storage) {
		// Get chalkboard drawings from session storage
		loaded = initStorage(sessionStorage.getItem(config.storage));
	}

	if (!loaded && config.src != null) {
		// Get chalkboard drawings from the given file
		loadData(config.src);
	}

	/**
	 * Initialize storage.
	 */
	function initStorage(json) {
		var success = false;
		try {
			var data = JSON.parse(json);
			for (var id = 0; id < data.length; id++) {
				if (drawingCanvas[id].width != data[id].width || drawingCanvas[id].height != data[id].height) {
					drawingCanvas[id].scale = Math.min(drawingCanvas[id].width / data[id].width, drawingCanvas[id].height / data[id].height);
					drawingCanvas[id].xOffset = (drawingCanvas[id].width - data[id].width * drawingCanvas[id].scale) / 2;
					drawingCanvas[id].yOffset = (drawingCanvas[id].height - data[id].height * drawingCanvas[id].scale) / 2;
				}
				if (config.readOnly) {
					drawingCanvas[id].container.style.cursor = 'default';
					drawingCanvas[id].canvas.style.cursor = 'default';
				}
			}
			success = true;
			storage = data;
		} catch (err) {
			console.warn("Cannot initialise storage!");
		}
		return success;
	}

	/**
	 * Load data.
	 */
	function loadData(filename) {
		var xhr = new XMLHttpRequest();
		xhr.onload = function () {
			if (xhr.readyState === 4 && xhr.status != 404) {
				loaded = initStorage(xhr.responseText);
				updateStorage();
				console.log("Drawings loaded from file");
			} else {
				config.readOnly = undefined;
				readOnly = undefined;
				console.warn('Failed to get file ' + filename + '. ReadyState: ' + xhr.readyState + ', Status: ' + xhr.status);
				loaded = false;
			}
		};

		xhr.open('GET', filename, true);
		try {
			xhr.send();
		} catch (error) {
			config.readOnly = undefined;
			readOnly = undefined;
			console.warn('Failed to get file ' + filename + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + error);
			loaded = false;
		}
	}

	function storageChanged(now) {
		if (!now) {
			// create or update timer
			if (updateStorageTimeout) {
				clearTimeout(updateStorageTimeout);
			}
			updateStorageTimeout = setTimeout(storageChanged, 1000, true);
		}
		else {
			// console.log("Update storage", updateStorageTimeout,  Date.now());
			updateStorage();
			updateStorageTimeout = null;
		}
	}

	function updateStorage() {
		var json = JSON.stringify(storage)
		if (config.storage) {
			sessionStorage.setItem(config.storage, json)
		}
		return json;
	}

	function recordEvent(event) {
		//console.log(event);
		event.time = Date.now() - slideStart;
		if (mode == 1) event.board = board;
		var slideData = getSlideData();
		var i = slideData.events.length;
		while (i > 0 && event.time < slideData.events[i - 1].time) {
			i--;
		}
		slideData.events.splice(i, 0, event);
		slideData.duration = Math.max(slideData.duration, Date.now() - slideStart) + 1;

		storageChanged();
	}

	/**
	 * Get data as json string.
	 */
	function getData() {
		// cleanup slide data without events
		for (var id = 0; id < 2; id++) {
			for (var i = storage[id].data.length - 1; i >= 0; i--) {
				if (storage[id].data[i].events.length == 0) {
					storage[id].data.splice(i, 1);
				}
			}
		}

		return updateStorage();
	}

	/**
	 * Download data.
	 */
	function downloadData() {
		var a = document.createElement('a');
		document.body.appendChild(a);
		try {
			a.download = 'chalkboard.json';
			var blob = new Blob([getData()], {
				type: 'application/json'
			});
			a.href = window.URL.createObjectURL(blob);
		} catch (error) {
			a.innerHTML += ' (' + error + ')';
		}
		a.click();
		document.body.removeChild(a);
	}

	/**
	 * Returns data object for the slide with the given indices.
	 */
	function getSlideData(indices, id) {
		if (id == undefined) id = mode;
		if (!indices) indices = slideIndices;
		var data;
		for (var i = 0; i < storage[id].data.length; i++) {
			if (storage[id].data[i].slide.h === indices.h && storage[id].data[i].slide.v === indices.v && storage[id].data[i].slide.f === indices.f) {
				data = storage[id].data[i];
				return data;
			}
		}
		var page = Number(Reveal.getCurrentSlide().getAttribute('data-pdf-page-number'));
		//console.log( indices, Reveal.getCurrentSlide() );
		storage[id].data.push({
			slide: indices,
			page,
			events: [],
			duration: 0
		});
		data = storage[id].data[storage[id].data.length - 1];
		return data;
	}

	/**
	 * Returns maximum duration of slide playback for both modes
	 */
	function getSlideDuration(indices) {
		if (!indices) indices = slideIndices;
		var duration = 0;
		for (var id = 0; id < 2; id++) {
			for (var i = 0; i < storage[id].data.length; i++) {
				if (storage[id].data[i].slide.h === indices.h && storage[id].data[i].slide.v === indices.v && storage[id].data[i].slide.f === indices.f) {
					duration = Math.max(duration, storage[id].data[i].duration);
					break;
				}
			}
		}
		//console.log( duration );
		return duration;
	}

	/*****************************************************************
	 ** Print
	 ******************************************************************/
	var printMode = (/print-pdf/gi).test(window.location.search);
	//console.log("createPrintout" + printMode)

	function addPageNumbers() {
		// determine page number for printouts with fragments serialised
		var slides = Reveal.getSlides();
		var page = 0;
		for (var i = 0; i < slides.length; i++) {
			slides[i].setAttribute('data-pdf-page-number', page.toString());
			// add number of fragments without fragment indices
			var count = slides[i].querySelectorAll('.fragment:not([data-fragment-index])').length;
			var fragments = slides[i].querySelectorAll('.fragment[data-fragment-index]');
			for (var j = 0; j < fragments.length; j++) {
				// increasenumber of fragments by highest fragment index (which start at 0)
				if (Number(fragments[j].getAttribute('data-fragment-index')) + 1 > count) {
					count = Number(fragments[j].getAttribute('data-fragment-index')) + 1;
				}
			}
			//console.log(count,fragments.length,( slides[i].querySelector('h1,h2,h3,h4')||{}).innerHTML, page);
			page += count + 1;
		}
	}

	function createPrintout() {
		//console.warn(Reveal.getTotalSlides(),Reveal.getSlidesElement());
		if (storage[1].data.length == 0) return;
		console.log('Create printout(s) for ' + storage[1].data.length + " slides");
		drawingCanvas[0].container.style.opacity = 0; // do not print notes canvas
		drawingCanvas[0].container.style.visibility = 'hidden';

		var patImg = new Image();
		patImg.onload = function () {
			var slides = Reveal.getSlides();
			//console.log(slides);
			for (var i = storage[1].data.length - 1; i >= 0; i--) {
				console.log('Create printout for slide ' + storage[1].data[i].slide.h + '.' + storage[1].data[i].slide.v);
				var slideData = getSlideData(storage[1].data[i].slide, 1);
				var drawings = createDrawings(slideData, patImg);
				//console.log("Page:", storage[ 1 ].data[ i ].page );
				//console.log("Slide:", slides[storage[ 1 ].data[ i ].page] );
				addDrawings(slides[storage[1].data[i].page], drawings);

			}
			//			Reveal.sync();
		};
		patImg.src = background[1];
	}

	function cloneCanvas(oldCanvas) {
		//create a new canvas
		var newCanvas = document.createElement('canvas');
		var context = newCanvas.getContext('2d');
		//set dimensions
		newCanvas.width = oldCanvas.width;
		newCanvas.height = oldCanvas.height;
		//apply the old canvas to the new one
		context.drawImage(oldCanvas, 0, 0);
		//return the new canvas
		return newCanvas;
	}

	function getCanvas(template, container, board) {
		var idx = container.findIndex(element => element.board === board);
		if (idx === -1) {
			var canvas = cloneCanvas(template);
			if (!container.length) {
				idx = 0;
				container.push({
					board,
					canvas
				});
			} else if (board < container[0].board) {
				idx = 0;
				container.unshift({
					board,
					canvas
				});
			} else if (board > container[container.length - 1].board) {
				idx = container.length;
				container.push({
					board,
					canvas
				});
			}
		}

		return container[idx].canvas;
	}

	function createDrawings(slideData, patImg) {
		var width = Reveal.getConfig().width;
		var height = Reveal.getConfig().height;
		var scale = 1;
		var xOffset = 0;
		var yOffset = 0;
		if (width != storage[1].width || height != storage[1].height) {
			scale = Math.min(width / storage[1].width, height / storage[1].height);
			xOffset = (width - storage[1].width * scale) / 2;
			yOffset = (height - storage[1].height * scale) / 2;
		}
		mode = 1;
		board = 0;
		//		console.log( 'Create printout(s) for slide ', slideData );

		var drawings = [];
		var template = document.createElement('canvas');
		template.width = width;
		template.height = height;

		var imgCtx = template.getContext('2d');
		imgCtx.fillStyle = imgCtx.createPattern(patImg, 'repeat');
		imgCtx.rect(0, 0, width, height);
		imgCtx.fill();

		for (var j = 0; j < slideData.events.length; j++) {
			switch (slideData.events[j].type) {
				case 'draw':
					draw[1](getCanvas(template, drawings, board).getContext('2d'),
						xOffset + slideData.events[j].x1 * scale,
						yOffset + slideData.events[j].y1 * scale,
						xOffset + slideData.events[j].x2 * scale,
						yOffset + slideData.events[j].y2 * scale,
						yOffset + slideData.events[j].color
					);
					break;
				case 'erase':
					eraseWithSponge(getCanvas(template, drawings, board).getContext('2d'),
						xOffset + slideData.events[j].x * scale,
						yOffset + slideData.events[j].y * scale
					);
					break;
				case 'selectboard':
					selectBoard(slideData.events[j].board);
					break;
				case 'clear':
					getCanvas(template, drawings, board).getContext('2d').clearRect(0, 0, width, height);
					getCanvas(template, drawings, board).getContext('2d').fill();
					break;
				default:
					break;
			}
		}

		drawings = drawings.sort((a, b) => a.board > b.board && 1 || -1);

		mode = 0;

		return drawings;
	}

	function addDrawings(slide, drawings) {
		var parent = slide.parentElement.parentElement;
		var nextSlide = slide.parentElement.nextElementSibling;

		for (var i = 0; i < drawings.length; i++) {
			var newPDFPage = document.createElement('div');
			newPDFPage.classList.add('pdf-page');
			newPDFPage.style.height = Reveal.getConfig().height;
			newPDFPage.append(drawings[i].canvas);
			//console.log("Add drawing", newPDFPage);
			if (nextSlide != null) {
				parent.insertBefore(newPDFPage, nextSlide);
			} else {
				parent.append(newPDFPage);
			}
		}
	}

	/*****************************************************************
	 ** Drawings
	 ******************************************************************/

	function drawWithBoardmarker(context, fromX, fromY, toX, toY, colorIdx) {
		if (colorIdx == undefined) colorIdx = color[mode];
		context.lineWidth = boardmarkerWidth;
		context.lineCap = 'round';
		context.strokeStyle = boardmarkers[colorIdx].color;
		context.beginPath();
		context.moveTo(fromX, fromY);
		context.lineTo(toX, toY);
		context.stroke();
	}

	function drawWithChalk(context, fromX, fromY, toX, toY, colorIdx) {
		if (colorIdx == undefined) colorIdx = color[mode];
		var brushDiameter = chalkWidth;
		context.lineWidth = brushDiameter;
		context.lineCap = 'round';
		context.fillStyle = chalks[colorIdx].color; // 'rgba(255,255,255,0.5)';
		context.strokeStyle = chalks[colorIdx].color;
		/*var opacity = Math.min(0.8, Math.max(0,color[1].replace(/^.*,(.+)\)/,'$1') - 0.1)) + Math.random()*0.2;*/
		var opacity = 1.0;
		context.strokeStyle = context.strokeStyle.replace(/[\d\.]+\)$/g, opacity + ')');
		context.beginPath();
		context.moveTo(fromX, fromY);
		context.lineTo(toX, toY);
		context.stroke();
		// Chalk Effect
		var length = Math.round(Math.sqrt(Math.pow(toX - fromX, 2) + Math.pow(toY - fromY, 2)) / (5 / brushDiameter));
		var xUnit = (toX - fromX) / length;
		var yUnit = (toY - fromY) / length;
		for (var i = 0; i < length; i++) {
			if (chalkEffect > (Math.random() * 0.9)) {
				var xCurrent = fromX + (i * xUnit);
				var yCurrent = fromY + (i * yUnit);
				var xRandom = xCurrent + (Math.random() - 0.5) * brushDiameter * 1.2;
				var yRandom = yCurrent + (Math.random() - 0.5) * brushDiameter * 1.2;
				context.clearRect(xRandom, yRandom, Math.random() * 2 + 2, Math.random() + 1);
			}
		}
	}

	function eraseWithSponge(context, x, y) {
		context.save();
		context.beginPath();
		context.arc(x, y, eraser.radius, 0, 2 * Math.PI, false);
		context.clip();
		context.clearRect(x - eraser.radius - 1, y - eraser.radius - 1, eraser.radius * 2 + 2, eraser.radius * 2 + 2);
		context.restore();
		if (mode == 1 && grid) {
			redrawGrid(x, y, eraser.radius);
		}
	}

	/**
	 * Show an overlay for the chalkboard.
	 */
	function showChalkboard() {
		//console.log("showChalkboard");
		clearTimeout(touchTimeout);
		touchTimeout = null;
		drawingCanvas[0].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
		drawingCanvas[1].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
		drawingCanvas[1].container.style.opacity = 1;
		drawingCanvas[1].container.style.visibility = 'visible';
		mode = 1;
	}

	/**
	 * Closes open chalkboard.
	 */
	function closeChalkboard() {
		clearTimeout(touchTimeout);
		touchTimeout = null;
		drawingCanvas[0].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
		drawingCanvas[1].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
		drawingCanvas[1].container.style.opacity = 0;
		drawingCanvas[1].container.style.visibility = 'hidden';
		lastX = null;
		lastY = null;
		mode = 0;
	}

	/**
	 * Clear current canvas.
	 */
	function clearCanvas(id) {
		if (id == 0) clearTimeout(slidechangeTimeout);
		drawingCanvas[id].context.clearRect(0, 0, drawingCanvas[id].width, drawingCanvas[id].height);
		if (id == 1 && grid) drawGrid();
	}

	/**
	 * Draw grid on background
	 */
	function drawGrid() {
		var context = drawingCanvas[1].context;

		drawingCanvas[1].scale = Math.min(drawingCanvas[1].width / storage[1].width, drawingCanvas[1].height / storage[1].height);
		drawingCanvas[1].xOffset = (drawingCanvas[1].width - storage[1].width * drawingCanvas[1].scale) / 2;
		drawingCanvas[1].yOffset = (drawingCanvas[1].height - storage[1].height * drawingCanvas[1].scale) / 2;

		var scale = drawingCanvas[1].scale;
		var xOffset = drawingCanvas[1].xOffset;
		var yOffset = drawingCanvas[1].yOffset;

		var distance = grid.distance * scale;

		var fromX = drawingCanvas[1].width / 2 - distance / 2 - Math.floor((drawingCanvas[1].width - distance) / 2 / distance) * distance;
		for (var x = fromX; x < drawingCanvas[1].width; x += distance) {
			context.beginPath();
			context.lineWidth = grid.width * scale;
			context.lineCap = 'round';
			context.fillStyle = grid.color;
			context.strokeStyle = grid.color;
			context.moveTo(x, 0);
			context.lineTo(x, drawingCanvas[1].height);
			context.stroke();
		}
		var fromY = drawingCanvas[1].height / 2 - distance / 2 - Math.floor((drawingCanvas[1].height - distance) / 2 / distance) * distance;

		for (var y = fromY; y < drawingCanvas[1].height; y += distance) {
			context.beginPath();
			context.lineWidth = grid.width * scale;
			context.lineCap = 'round';
			context.fillStyle = grid.color;
			context.strokeStyle = grid.color;
			context.moveTo(0, y);
			context.lineTo(drawingCanvas[1].width, y);
			context.stroke();
		}
	}

	function redrawGrid(centerX, centerY, diameter) {
		var context = drawingCanvas[1].context;

		drawingCanvas[1].scale = Math.min(drawingCanvas[1].width / storage[1].width, drawingCanvas[1].height / storage[1].height);
		drawingCanvas[1].xOffset = (drawingCanvas[1].width - storage[1].width * drawingCanvas[1].scale) / 2;
		drawingCanvas[1].yOffset = (drawingCanvas[1].height - storage[1].height * drawingCanvas[1].scale) / 2;

		var scale = drawingCanvas[1].scale;
		var xOffset = drawingCanvas[1].xOffset;
		var yOffset = drawingCanvas[1].yOffset;

		var distance = grid.distance * scale;

		var fromX = drawingCanvas[1].width / 2 - distance / 2 - Math.floor((drawingCanvas[1].width - distance) / 2 / distance) * distance;

		for (var x = fromX + distance * Math.ceil((centerX - diameter - fromX) / distance); x <= fromX + distance * Math.floor((centerX + diameter - fromX) / distance); x += distance) {
			context.beginPath();
			context.lineWidth = grid.width * scale;
			context.lineCap = 'round';
			context.fillStyle = grid.color;
			context.strokeStyle = grid.color;
			context.moveTo(x, centerY - Math.sqrt(diameter * diameter - (centerX - x) * (centerX - x)));
			context.lineTo(x, centerY + Math.sqrt(diameter * diameter - (centerX - x) * (centerX - x)));
			context.stroke();
		}
		var fromY = drawingCanvas[1].height / 2 - distance / 2 - Math.floor((drawingCanvas[1].height - distance) / 2 / distance) * distance;
		for (var y = fromY + distance * Math.ceil((centerY - diameter - fromY) / distance); y <= fromY + distance * Math.floor((centerY + diameter - fromY) / distance); y += distance) {
			context.beginPath();
			context.lineWidth = grid.width * scale;
			context.lineCap = 'round';
			context.fillStyle = grid.color;
			context.strokeStyle = grid.color;
			context.moveTo(centerX - Math.sqrt(diameter * diameter - (centerY - y) * (centerY - y)), y);
			context.lineTo(centerX + Math.sqrt(diameter * diameter - (centerY - y) * (centerY - y)), y);
			context.stroke();
		}
	}

	/**
	 * Set the  color
	 */
	function setColor(index, record) {
		// protect against out of bounds (this could happen when
		// replaying events recorded with different color settings).
		if (index >= pens[mode].length) index = 0;
		color[mode] = index;
		drawingCanvas[mode].canvas.style.cursor = pens[mode][color[mode]].cursor;
	}

	/**
	 * Set the  board
	 */
	function selectBoard(boardIdx, record) {
		//console.log("Set board",boardIdx);
		if (board == boardIdx) return;

		board = boardIdx;
		redrawChalkboard(boardIdx);
		if (record) {
			recordEvent({ type: 'selectboard' });
		}
	}

	function redrawChalkboard(boardIdx) {
		clearCanvas(1);
		var slideData = getSlideData(slideIndices, 1);
		var index = 0;
		var play = (boardIdx == 0);
		while (index < slideData.events.length && slideData.events[index].time < Date.now() - slideStart) {
			if (boardIdx == slideData.events[index].board) {
				playEvent(1, slideData.events[index], Date.now() - slideStart);
			}

			index++;
		}
	}

	/**
	 * Forward cycle color
	 */
	function cycleColorNext() {
		color[mode] = (color[mode] + 1) % pens[mode].length;
		return color[mode];
	}

	/**
	 * Backward cycle color
	 */
	function cycleColorPrev() {
		color[mode] = (color[mode] + (pens[mode].length - 1)) % pens[mode].length;
		return color[mode];
	}

	/*****************************************************************
	 ** Broadcast
	 ******************************************************************/

	var eventQueue = [];

	document.addEventListener('received', function (message) {
		if (message.content && message.content.sender == 'chalkboard-plugin') {
			// add message to queue
			eventQueue.push(message);
			console.log(JSON.stringify(message));
		}
		if (eventQueue.length == 1) processQueue();
	});

	function processQueue() {
		// take first message from queue
		var message = eventQueue.shift();

		// synchronize time with seminar host
		slideStart = Date.now() - message.content.timestamp;
		// set status
		if (mode < message.content.mode) {
			// open chalkboard
			showChalkboard();
		} else if (mode > message.content.mode) {
			// close chalkboard
			closeChalkboard();
		}
		if (board != message.content.board) {
			board = message.content.board;
			redrawChalkboard(board);
		};

		switch (message.content.type) {
			case 'showChalkboard':
				showChalkboard();
				break;
			case 'closeChalkboard':
				closeChalkboard();
				break;
			case 'erase':
				erasePoint(message.content.x, message.content.y);
				break;
			case 'draw':
				drawSegment(message.content.fromX, message.content.fromY, message.content.toX, message.content.toY, message.content.color);
				break;
			case 'clear':
				clearSlide();
				break;
			case 'selectboard':
				selectBoard(message.content.board, true);
				break;
			case 'resetSlide':
				resetSlideDrawings();
				break;
			case 'init':
				storage = message.content.storage;
				for (var id = 0; id < 2; id++) {
					drawingCanvas[id].scale = Math.min(drawingCanvas[id].width / storage[id].width, drawingCanvas[id].height / storage[id].height);
					drawingCanvas[id].xOffset = (drawingCanvas[id].width - storage[id].width * drawingCanvas[id].scale) / 2;
					drawingCanvas[id].yOffset = (drawingCanvas[id].height - storage[id].height * drawingCanvas[id].scale) / 2;
				}
				clearCanvas(0);
				clearCanvas(1);
				if (!playback) {
					slidechangeTimeout = setTimeout(startPlayback, transition, getSlideDuration(), 0);
				}
				if (mode == 1 && message.content.mode == 0) {
					setTimeout(closeChalkboard, transition + 50);
				}
				if (mode == 0 && message.content.mode == 1) {
					setTimeout(showChalkboard, transition + 50);
				}
				mode = message.content.mode;
				board = message.content.board;
				break;
			default:
				break;
		}

		// continue with next message if queued
		if (eventQueue.length > 0) {
			processQueue();
		} else {
			storageChanged();
		}
	}

	document.addEventListener('welcome', function (user) {
		// broadcast storage
		var message = new CustomEvent(messageType);
		message.content = {
			sender: 'chalkboard-plugin',
			recipient: user.id,
			type: 'init',
			timestamp: Date.now() - slideStart,
			storage: storage,
			mode,
			board
		};
		document.dispatchEvent(message);
	});

	/*****************************************************************
	 ** Playback
	 ******************************************************************/

	document.addEventListener('seekplayback', function (event) {
		//console.log('event seekplayback ' + event.timestamp);
		stopPlayback();
		if (!playback || event.timestamp == 0) {
			// in other cases startplayback fires after seeked
			startPlayback(event.timestamp);
		}
		//console.log('seeked');
	});

	document.addEventListener('startplayback', function (event) {
		//console.log('event startplayback ' + event.timestamp);
		stopPlayback();
		playback = true;
		startPlayback(event.timestamp);
	});

	document.addEventListener('stopplayback', function (event) {
		//console.log('event stopplayback ' + (Date.now() - slideStart) );
		playback = false;
		stopPlayback();
	});

	document.addEventListener('startrecording', function (event) {
		//console.log('event startrecording ' + event.timestamp);
		startRecording();
	});

	function startRecording() {
		resetSlide(true);
		slideStart = Date.now();
	}

	function startPlayback(timestamp, finalMode) {
		//console.log("playback " + timestamp );
		slideStart = Date.now() - timestamp;
		closeChalkboard();
		mode = 0;
		board = 0;
		for (var id = 0; id < 2; id++) {
			clearCanvas(id);
			var slideData = getSlideData(slideIndices, id);
			//console.log( timestamp +" / " + JSON.stringify(slideData));
			var index = 0;
			while (index < slideData.events.length && slideData.events[index].time < (Date.now() - slideStart)) {
				playEvent(id, slideData.events[index], timestamp);
				index++;
			}

			while (playback && index < slideData.events.length) {
				timeouts[id].push(setTimeout(playEvent, slideData.events[index].time - (Date.now() - slideStart), id, slideData.events[index], timestamp));
				index++;
			}
		}
		//console.log("Mode: " + finalMode + "/" + mode );
		if (finalMode != undefined) {
			mode = finalMode;
		}
		if (mode == 1) showChalkboard();
		//console.log("playback (ok)");

	};

	function stopPlayback() {
		//console.log("stopPlayback");
		//console.log("Timeouts: " + timeouts[0].length + "/"+ timeouts[1].length);
		for (var id = 0; id < 2; id++) {
			for (var i = 0; i < timeouts[id].length; i++) {
				clearTimeout(timeouts[id][i]);
			}
			timeouts[id] = [];
		}
	};

	function playEvent(id, event, timestamp) {
		//console.log( timestamp +" / " + JSON.stringify(event));
		//console.log( id + ": " + timestamp +" / " +  event.time +" / " + event.type +" / " + mode );
		switch (event.type) {
			case 'open':
				if (timestamp <= event.time) {
					showChalkboard();
				} else {
					mode = 1;
				}

				break;
			case 'close':
				if (timestamp < event.time) {
					closeChalkboard();
				} else {
					mode = 0;
				}
				break;
			case 'clear':
				clearCanvas(id);
				break;
			case 'selectboard':
				selectBoard(event.board);
				break;
			case 'draw':
				drawLine(id, event, timestamp);
				break;
			case 'erase':
				eraseCircle(id, event, timestamp);
				break;
		}
	};

	function drawLine(id, event, timestamp) {
		var ctx = drawingCanvas[id].context;
		var scale = drawingCanvas[id].scale;
		var xOffset = drawingCanvas[id].xOffset;
		var yOffset = drawingCanvas[id].yOffset;
		draw[id](ctx, xOffset + event.x1 * scale, yOffset + event.y1 * scale, xOffset + event.x2 * scale, yOffset + event.y2 * scale, event.color);
	};

	function eraseCircle(id, event, timestamp) {
		var ctx = drawingCanvas[id].context;
		var scale = drawingCanvas[id].scale;
		var xOffset = drawingCanvas[id].xOffset;
		var yOffset = drawingCanvas[id].yOffset;

		eraseWithSponge(ctx, xOffset + event.x * scale, yOffset + event.y * scale);
	};

	function startErasing(x, y) {
		drawing = false;
		erasing = true;
		drawingCanvas[mode].sponge.style.visibility = 'visible';
		erasePoint(x, y);
	}

	function erasePoint(x, y) {
		var ctx = drawingCanvas[mode].context;
		var scale = drawingCanvas[mode].scale;
		var xOffset = drawingCanvas[mode].xOffset;
		var yOffset = drawingCanvas[mode].yOffset;

		// move sponge image
		drawingCanvas[mode].sponge.style.left = (x * scale + xOffset - eraser.radius) + 'px';
		drawingCanvas[mode].sponge.style.top = (y * scale + yOffset - 2 * eraser.radius) + 'px';

		recordEvent({
			type: 'erase',
			x,
			y
		});

		if (
			x * scale + xOffset > 0 &&
			y * scale + yOffset > 0 &&
			x * scale + xOffset < drawingCanvas[mode].width &&
			y * scale + yOffset < drawingCanvas[mode].height
		) {
			eraseWithSponge(ctx, x * scale + xOffset, y * scale + yOffset);
		}
	}

	function stopErasing() {
		erasing = false;
		// hide sponge
		drawingCanvas[mode].sponge.style.visibility = 'hidden';
	}

	function startDrawing(x, y) {
		drawing = true;

		var ctx = drawingCanvas[mode].context;
		var scale = drawingCanvas[mode].scale;
		var xOffset = drawingCanvas[mode].xOffset;
		var yOffset = drawingCanvas[mode].yOffset;
		lastX = x * scale + xOffset;
		lastY = y * scale + yOffset;
	}

	function drawSegment(fromX, fromY, toX, toY, colorIdx) {
		var ctx = drawingCanvas[mode].context;
		var scale = drawingCanvas[mode].scale;
		var xOffset = drawingCanvas[mode].xOffset;
		var yOffset = drawingCanvas[mode].yOffset;

		recordEvent({
			type: 'draw',
			color: colorIdx,
			x1: fromX,
			y1: fromY,
			x2: toX,
			y2: toY
		});

		if (
			fromX * scale + xOffset > 0 &&
			fromY * scale + yOffset > 0 &&
			fromX * scale + xOffset < drawingCanvas[mode].width &&
			fromY * scale + yOffset < drawingCanvas[mode].height &&
			toX * scale + xOffset > 0 &&
			toY * scale + yOffset > 0 &&
			toX * scale + xOffset < drawingCanvas[mode].width &&
			toY * scale + yOffset < drawingCanvas[mode].height
		) {
			draw[mode](ctx, fromX * scale + xOffset, fromY * scale + yOffset, toX * scale + xOffset, toY * scale + yOffset, colorIdx);
		}
	}

	function stopDrawing() {
		drawing = false;
	}

	/*****************************************************************
	 ** User interface
	 ******************************************************************/

	function setupCanvasEvents(canvas) {
		// TODO: check all touchevents
		canvas.addEventListener('touchstart', function (evt) {
			evt.preventDefault();
			//console.log("Touch start");
			if (!readOnly && evt.target.getAttribute('data-chalkboard') == mode) {
				var scale = drawingCanvas[mode].scale;
				var xOffset = drawingCanvas[mode].xOffset;
				var yOffset = drawingCanvas[mode].yOffset;

				var touch = evt.touches[0];
				mouseX = touch.pageX;
				mouseY = touch.pageY;
				startDrawing((mouseX - xOffset) / scale, (mouseY - yOffset) / scale);
				touchTimeout = setTimeout(startErasing, 500, (mouseX - xOffset) / scale, (mouseY - yOffset) / scale);
			}
		}, passiveSupported ? {
			passive: false
		} : false);

		canvas.addEventListener('touchmove', function (evt) {
			evt.preventDefault();
			//console.log("Touch move");
			clearTimeout(touchTimeout);
			touchTimeout = null;
			if (drawing || erasing) {
				var scale = drawingCanvas[mode].scale;
				var xOffset = drawingCanvas[mode].xOffset;
				var yOffset = drawingCanvas[mode].yOffset;

				var touch = evt.touches[0];
				mouseX = touch.pageX;
				mouseY = touch.pageY;
				if (mouseY < drawingCanvas[mode].height && mouseX < drawingCanvas[mode].width) {
					// move sponge
					if (event.type == 'erase') {
						drawingCanvas[mode].sponge.style.left = (mouseX - eraser.radius) + 'px';
						drawingCanvas[mode].sponge.style.top = (mouseY - eraser.radius) + 'px';
					}
				}

				if (drawing) {
					drawSegment((lastX - xOffset) / scale, (lastY - yOffset) / scale, (mouseX - xOffset) / scale, (mouseY - yOffset) / scale, color[mode]);
					// broadcast
					var message = new CustomEvent(messageType);
					message.content = {
						sender: 'chalkboard-plugin',
						type: 'draw',
						timestamp: Date.now() - slideStart,
						mode,
						board,
						fromX: (lastX - xOffset) / scale,
						fromY: (lastY - yOffset) / scale,
						toX: (mouseX - xOffset) / scale,
						toY: (mouseY - yOffset) / scale,
						color: color[mode]
					};
					document.dispatchEvent(message);

					lastX = mouseX;
					lastY = mouseY;
				} else {
					erasePoint((mouseX - xOffset) / scale, (mouseY - yOffset) / scale);
					// broadcast
					var message = new CustomEvent(messageType);
					message.content = {
						sender: 'chalkboard-plugin',
						type: 'erase',
						timestamp: Date.now() - slideStart,
						mode,
						board,
						x: (mouseX - xOffset) / scale,
						y: (mouseY - yOffset) / scale
					};
					document.dispatchEvent(message);
				}

			}
		}, false);

		canvas.addEventListener('touchend', function (evt) {
			evt.preventDefault();
			clearTimeout(touchTimeout);
			touchTimeout = null;
			// hide sponge image
			drawingCanvas[mode].sponge.style.visibility = 'hidden';
			stopDrawing();
		}, false);

		canvas.addEventListener('mousedown', function (evt) {
			evt.preventDefault();
			if (!readOnly && evt.target.getAttribute('data-chalkboard') == mode) {
				//console.log( "mousedown: " + evt.button );
				var scale = drawingCanvas[mode].scale;
				var xOffset = drawingCanvas[mode].xOffset;
				var yOffset = drawingCanvas[mode].yOffset;

				mouseX = evt.pageX;
				mouseY = evt.pageY;

				if (evt.button == 2 || evt.button == 1) {
					startErasing((mouseX - xOffset) / scale, (mouseY - yOffset) / scale);
					// broadcast
					var message = new CustomEvent(messageType);
					message.content = {
						sender: 'chalkboard-plugin',
						type: 'erase',
						timestamp: Date.now() - slideStart,
						mode,
						board,
						x: (mouseX - xOffset) / scale,
						y: (mouseY - yOffset) / scale
					};
					document.dispatchEvent(message);
				} else {
					startDrawing((mouseX - xOffset) / scale, (mouseY - yOffset) / scale);
				}
			}
		});

		canvas.addEventListener('mousemove', function (evt) {
			evt.preventDefault();
			//console.log("Mouse move");
			if (drawing || erasing) {
				var scale = drawingCanvas[mode].scale;
				var xOffset = drawingCanvas[mode].xOffset;
				var yOffset = drawingCanvas[mode].yOffset;

				mouseX = evt.pageX;
				mouseY = evt.pageY;

				if (drawing) {
					drawSegment((lastX - xOffset) / scale, (lastY - yOffset) / scale, (mouseX - xOffset) / scale, (mouseY - yOffset) / scale, color[mode]);
					// broadcast
					var message = new CustomEvent(messageType);
					message.content = {
						sender: 'chalkboard-plugin',
						type: 'draw',
						timestamp: Date.now() - slideStart,
						mode,
						board,
						fromX: (lastX - xOffset) / scale,
						fromY: (lastY - yOffset) / scale,
						toX: (mouseX - xOffset) / scale,
						toY: (mouseY - yOffset) / scale,
						color: color[mode]
					};
					document.dispatchEvent(message);

					lastX = mouseX;
					lastY = mouseY;
				} else {
					erasePoint((mouseX - xOffset) / scale, (mouseY - yOffset) / scale);
					// broadcast
					var message = new CustomEvent(messageType);
					message.content = {
						sender: 'chalkboard-plugin',
						type: 'erase',
						timestamp: Date.now() - slideStart,
						mode,
						board,
						x: (mouseX - xOffset) / scale,
						y: (mouseY - yOffset) / scale
					};
					document.dispatchEvent(message);
				}

			}
		});

		canvas.addEventListener('mouseup', function (evt) {
			evt.preventDefault();
			drawingCanvas[mode].canvas.style.cursor = pens[mode][color[mode]].cursor;
			if (drawing || erasing) {
				stopDrawing();
				stopErasing();
			}
		});
	}

	function resize() {
		//console.log("resize");
		// Resize the canvas and draw everything again
		var timestamp = Date.now() - slideStart;
		if (!playback) {
			timestamp = getSlideDuration();
		}

		//console.log( drawingCanvas[0].scale + "/" + drawingCanvas[0].xOffset + "/" +drawingCanvas[0].yOffset );
		for (var id = 0; id < 2; id++) {
			drawingCanvas[id].width = window.innerWidth;
			drawingCanvas[id].height = window.innerHeight;
			drawingCanvas[id].canvas.width = drawingCanvas[id].width;
			drawingCanvas[id].canvas.height = drawingCanvas[id].height;
			drawingCanvas[id].context.canvas.width = drawingCanvas[id].width;
			drawingCanvas[id].context.canvas.height = drawingCanvas[id].height;

			drawingCanvas[id].scale = Math.min(drawingCanvas[id].width / storage[id].width, drawingCanvas[id].height / storage[id].height);
			drawingCanvas[id].xOffset = (drawingCanvas[id].width - storage[id].width * drawingCanvas[id].scale) / 2;
			drawingCanvas[id].yOffset = (drawingCanvas[id].height - storage[id].height * drawingCanvas[id].scale) / 2;
			//console.log( drawingCanvas[id].scale + "/" + drawingCanvas[id].xOffset + "/" +drawingCanvas[id].yOffset );
		}
		//console.log( window.innerWidth + "/" + window.innerHeight);
		startPlayback(timestamp, mode, true);
	}

	Reveal.addEventListener('pdf-ready', function (evt) {
		//		console.log( "Create printouts when ready" );
		whenLoaded(createPrintout);
	});

	Reveal.addEventListener('ready', function (evt) {
		//console.log('ready');
		if (!printMode) {
			window.addEventListener('resize', resize);

			slideStart = Date.now() - getSlideDuration();
			slideIndices = Reveal.getIndices();
			if (!playback) {
				startPlayback(getSlideDuration(), 0);
			}
			if (Reveal.isAutoSliding()) {
				var event = new CustomEvent('startplayback');
				event.timestamp = 0;
				document.dispatchEvent(event);
			}
			updateStorage();
			whenReady(addPageNumbers);
		}
	});
	Reveal.addEventListener('slidechanged', function (evt) {
		//		clearTimeout( slidechangeTimeout );
		//console.log('slidechanged');
		if (!printMode) {
			slideStart = Date.now() - getSlideDuration();
			slideIndices = Reveal.getIndices();
			closeChalkboard();
			board = 0;
			clearCanvas(0);
			clearCanvas(1);
			if (!playback) {
				slidechangeTimeout = setTimeout(startPlayback, transition, getSlideDuration(), 0);
			}
			if (Reveal.isAutoSliding()) {
				var event = new CustomEvent('startplayback');
				event.timestamp = 0;
				document.dispatchEvent(event);
			}
		}
	});
	Reveal.addEventListener('fragmentshown', function (evt) {
		//		clearTimeout( slidechangeTimeout );
		//console.log('fragmentshown');
		if (!printMode) {
			slideStart = Date.now() - getSlideDuration();
			slideIndices = Reveal.getIndices();
			closeChalkboard();
			board = 0;
			clearCanvas(0);
			clearCanvas(1);
			if (Reveal.isAutoSliding()) {
				var event = new CustomEvent('startplayback');
				event.timestamp = 0;
				document.dispatchEvent(event);
			} else if (!playback) {
				startPlayback(getSlideDuration(), 0);
				//				closeChalkboard();
			}
		}
	});
	Reveal.addEventListener('fragmenthidden', function (evt) {
		//		clearTimeout( slidechangeTimeout );
		//console.log('fragmenthidden');
		if (!printMode) {
			slideStart = Date.now() - getSlideDuration();
			slideIndices = Reveal.getIndices();
			closeChalkboard();
			board = 0;
			clearCanvas(0);
			clearCanvas(1);
			if (Reveal.isAutoSliding()) {
				document.dispatchEvent(new CustomEvent('stopplayback'));
			} else if (!playback) {
				startPlayback(getSlideDuration());
				closeChalkboard();
			}
		}
	});

	Reveal.addEventListener('autoslideresumed', function (evt) {
		//console.log('autoslideresumed');
		var event = new CustomEvent('startplayback');
		event.timestamp = 0;
		document.dispatchEvent(event);
	});
	Reveal.addEventListener('autoslidepaused', function (evt) {
		//console.log('autoslidepaused');
		document.dispatchEvent(new CustomEvent('stopplayback'));

		// advance to end of slide
		//		closeChalkboard();
		startPlayback(getSlideDuration(), 0);
	});

	function toggleNotesCanvas() {
		if (!readOnly) {
			if (mode == 1) {
				toggleChalkboard();
				notescanvas.style.background = background[0]; //'rgba(255,0,0,0.5)';
				notescanvas.style.pointerEvents = 'auto';
			}
			else {
				if (notescanvas.style.pointerEvents != 'none') {
					// hide notes canvas
					if (colorButtons) {
						notescanvas.querySelector('.palette').style.visibility = 'hidden';
					}
					notescanvas.style.background = 'rgba(0,0,0,0)';
					notescanvas.style.pointerEvents = 'none';
				}
				else {
					// show notes canvas
					if (colorButtons) {
						notescanvas.querySelector('.palette').style.visibility = 'visible';
					}
					notescanvas.style.background = background[0]; //'rgba(255,0,0,0.5)';
					notescanvas.style.pointerEvents = 'auto';

					var idx = 0;
					if (color[mode]) {
						idx = color[mode];
					}

					setColor(idx, true);
				}
			}
		}
	};

	function toggleChalkboard() {
		//console.log("toggleChalkboard " + mode);
		if (mode == 1) {
			if (!readOnly) {
				recordEvent({ type: 'close' });
			}
			closeChalkboard();

			// broadcast
			var message = new CustomEvent(messageType);
			message.content = {
				sender: 'chalkboard-plugin',
				type: 'closeChalkboard',
				timestamp: Date.now() - slideStart,
				mode: 0,
				board
			};
			document.dispatchEvent(message);

		} else {
			showChalkboard();
			if (!readOnly) {
				recordEvent({ type: 'open' });
				// broadcast
				var message = new CustomEvent(messageType);
				message.content = {
					sender: 'chalkboard-plugin',
					type: 'showChalkboard',
					timestamp: Date.now() - slideStart,
					mode: 1,
					board
				};
				document.dispatchEvent(message);

				var idx = 0;

				if (rememberColor[mode]) {
					idx = color[mode];
				}

				setColor(idx, true);
			}
		}
	};

	function clearSlide() {
		recordEvent({ type: 'clear' });
		clearCanvas(mode);
	}

	function clear() {
		if (!readOnly) {
			clearSlide();
			// broadcast
			var message = new CustomEvent(messageType);
			message.content = {
				sender: 'chalkboard-plugin',
				type: 'clear',
				timestamp: Date.now() - slideStart,
				mode,
				board
			};
			document.dispatchEvent(message);
		}
	};

	function colorIndex(idx) {
		if (!readOnly) {
			setColor(idx, true);
		}
	}

	function colorNext() {
		if (!readOnly) {
			let idx = cycleColorNext();
			setColor(idx, true);
		}
	}

	function colorPrev() {
		if (!readOnly) {
			let idx = cycleColorPrev();
			setColor(idx, true);
		}
	}

	function resetSlideDrawings() {
		slideStart = Date.now();
		closeChalkboard();

		clearCanvas(0);
		clearCanvas(1);

		mode = 1;
		var slideData = getSlideData();
		slideData.duration = 0;
		slideData.events = [];
		mode = 0;
		var slideData = getSlideData();
		slideData.duration = 0;
		slideData.events = [];

		updateStorage();
	}

	function resetSlide(force) {
		var ok = force || confirm("Please confirm to delete chalkboard drawings on this slide!");
		if (ok) {
			//console.log("resetSlide ");
			stopPlayback();
			resetSlideDrawings();
			// broadcast
			var message = new CustomEvent(messageType);
			message.content = {
				sender: 'chalkboard-plugin',
				type: 'resetSlide',
				timestamp: Date.now() - slideStart,
				mode,
				board
			};
			document.dispatchEvent(message);
		}
	};

	function resetStorage(force) {
		var ok = force || confirm("Please confirm to delete all chalkboard drawings!");
		if (ok) {
			stopPlayback();
			slideStart = Date.now();
			clearCanvas(0);
			clearCanvas(1);
			if (mode == 1) {
				closeChalkboard();
			}

			storage = [{
				width: Reveal.getConfig().width,
				height: Reveal.getConfig().height,
				data: []
			},
			{
				width: Reveal.getConfig().width,
				height: Reveal.getConfig().height,
				data: []
			}
			];

			if (config.storage) {
				sessionStorage.setItem(config.storage, null)
			}
			// broadcast
			var message = new CustomEvent(messageType);
			message.content = {
				sender: 'chalkboard-plugin',
				type: 'init',
				timestamp: Date.now() - slideStart,
				storage,
				mode,
				board
			};
			document.dispatchEvent(message);
		}
	};

	this.toggleNotesCanvas = toggleNotesCanvas;
	this.toggleChalkboard = toggleChalkboard;
	this.colorIndex = colorIndex;
	this.colorNext = colorNext;
	this.colorPrev = colorPrev;
	this.clear = clear;
	this.reset = resetSlide;
	this.resetAll = resetStorage;
	this.download = downloadData;
	this.updateStorage = updateStorage;
	this.getData = getData;
	this.configure = configure;

	for (var key in keyBindings) {
		if (keyBindings[key]) {
			Reveal.addKeyBinding(keyBindings[key], RevealChalkboard[key]);
		}
	};

	return this;
};


text:
<?php

namespace SilverStripe\Admin\Tests;

use LogicException;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\BasicNestedObject;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\ManagedDataObject;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\CMSEditModelAdmin;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\NestedObject;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\PolymorphicNestedObject;
use SilverStripe\Dev\SapphireTest;

class CMSEditLinkExtensionTest extends SapphireTest
{
    protected static $fixture_file = 'CMSEditLinkExtensionTest.yml';

    protected $usesDatabase = true;

    protected static $extra_dataobjects = [
        ManagedDataObject::class,
        BasicNestedObject::class,
        NestedObject::class,
        PolymorphicNestedObject::class,
    ];

    protected static $extra_controllers = [
        CMSEditModelAdmin::class,
    ];

    public function testGetCMSEditOwner()
    {
        $adminSingleton = CMSEditModelAdmin::singleton();
        $root = $this->objFromFixture(ManagedDataObject::class, 'root');
        $basicNested = $this->objFromFixture(BasicNestedObject::class, 'one');
        $nested = $this->objFromFixture(NestedObject::class, 'one');
        $polymorphic = $this->objFromFixture(PolymorphicNestedObject::class, 'one');

        $this->assertSame($adminSingleton, $root->getCMSEditOwner());
        $this->assertSame($root->ID, $basicNested->getCMSEditOwner()->ID);
        $this->assertSame($root->ID, $nested->getCMSEditOwner()->ID);
        $this->assertSame($root->ID, $polymorphic->getCMSEditOwner()->ID);
    }

    public function testGetEditLinkForDataObject()
    {
        $root = $this->objFromFixture(ManagedDataObject::class, 'root');
        $basicNested = $this->objFromFixture(BasicNestedObject::class, 'one');
        $nested = $this->objFromFixture(NestedObject::class, 'one');
        $polymorphic = $this->objFromFixture(PolymorphicNestedObject::class, 'one');

        $rootUrl = "http://localhost/admin/cms-edit-test/belongsHere/EditForm/field/belongsHere/item/$root->ID";
        $this->assertSame(
            "$rootUrl/ItemEditForm/field/BasicNested/item/$basicNested->ID",
            $root->getCMSEditLinkForManagedDataObject($basicNested, 'Parent')
        );
        $this->assertSame(
            "$rootUrl/ItemEditForm/field/Nested/item/$nested->ID",
            $root->getCMSEditLinkForManagedDataObject($nested, 'Parent')
        );
        $this->assertSame(
            "$rootUrl/ItemEditForm/field/Polymorphic/item/$polymorphic->ID",
            $root->getCMSEditLinkForManagedDataObject($polymorphic, 'Parent')
        );
    }

    public function testGetEditLinkForDataObjectException()
    {
        $root = $this->objFromFixture(ManagedDataObject::class, 'root');
        $nested = $this->objFromFixture(NestedObject::class, 'redHerringOne');

        $this->expectException(LogicException::class);
        $this->assertNull($root->getCMSEditLinkForManagedDataObject($nested, 'AnotherOfTheSameClass'));
    }

    public function testCMSEditLink()
    {
        $root = $this->objFromFixture(ManagedDataObject::class, 'root');
        $basicNested = $this->objFromFixture(BasicNestedObject::class, 'one');
        $nested = $this->objFromFixture(NestedObject::class, 'one');
        $polymorphic = $this->objFromFixture(PolymorphicNestedObject::class, 'one');

        $rootUrl = "http://localhost/admin/cms-edit-test/belongsHere/EditForm/field/belongsHere/item/$root->ID";
        $this->assertSame($rootUrl, $root->CMSEditLink());
        $this->assertSame(
            "$rootUrl/ItemEditForm/field/BasicNested/item/$basicNested->ID",
            $basicNested->CMSEditLink()
        );
        $this->assertSame(
            "$rootUrl/ItemEditForm/field/Nested/item/$nested->ID",
            $nested->CMSEditLink()
        );
        $this->assertSame(
            "$rootUrl/ItemEditForm/field/Polymorphic/item/$polymorphic->ID",
            $polymorphic->CMSEditLink()
        );
    }
}

redpajama_set_name: RedPajamaGithub


text:
<?php

/**
 * Consulta form base class.
 *
 * @method Consulta getObject() Returns the current form's model object
 *
 * @package    symfony1-baseproject
 * @subpackage form
 * @author     mppfiles
 */
abstract class BaseConsultaForm extends BaseFormPropel
{
  public function setup()
  {
    $this->setWidgets(array(
      'id'                                => new sfWidgetFormInputHidden(),
      'tipo_proyecto_id'                  => new sfWidgetFormPropelChoice(array('model' => 'TipoProyecto', 'add_empty' => false)),
      'referencia'                        => new sfWidgetFormInputText(),
      'productor_id'                      => new sfWidgetFormPropelChoice(array('model' => 'Productor', 'add_empty' => false)),
      'ingresosanualesdeclarados'         => new sfWidgetFormInputText(),
      'costosanualesdeclarados'           => new sfWidgetFormInputText(),
      'estacionmayoresingresos'           => new sfWidgetFormInputText(),
      'estacionmayorescostos'             => new sfWidgetFormInputText(),
      'pregadicional1'                    => new sfWidgetFormInputCheckbox(),
      'pregadicional2'                    => new sfWidgetFormInputCheckbox(),
      'pregadicional3'                    => new sfWidgetFormInputCheckbox(),
      'asociacionesproductores'           => new sfWidgetFormInputText(),
      'garantiaspersonales'               => new sfWidgetFormInputCheckbox(),
      'garantiasdeterceros'               => new sfWidgetFormInputCheckbox(),
      'garantiasprendaohipoteca'          => new sfWidgetFormInputCheckbox(),
      'garantiasdescuentos'               => new sfWidgetFormInputCheckbox(),
      'garantiasotras'                    => new sfWidgetFormInputCheckbox(),
      'firmada'                           => new sfWidgetFormInputText(),
      'oficina_id'                        => new sfWidgetFormPropelChoice(array('model' => 'Oficina', 'add_empty' => false)),
      'estado'                            => new sfWidgetFormPropelChoice(array('model' => 'EstadoConsulta', 'add_empty' => false)),
      'observaciones'                     => new sfWidgetFormTextarea(),
      'created_at'                        => new sfWidgetFormDateTime(),
      'created_by'                        => new sfWidgetFormPropelChoice(array('model' => 'sfGuardUser', 'add_empty' => true)),
      'updated_at'                        => new sfWidgetFormDateTime(),
      'updated_by'                        => new sfWidgetFormPropelChoice(array('model' => 'sfGuardUser', 'add_empty' => true)),
      'actividadconsultas_list'           => new sfWidgetFormPropelChoice(array('multiple' => true, 'model' => 'Actividad')),
      'consulta_estaciones_costos_list'   => new sfWidgetFormPropelChoice(array('multiple' => true, 'model' => 'Estacion')),
      'consulta_estaciones_ingresos_list' => new sfWidgetFormPropelChoice(array('multiple' => true, 'model' => 'Estacion')),
    ));

    $this->setValidators(array(
      'id'                                => new sfValidatorChoice(array('choices' => array($this->getObject()->getId()), 'empty_value' => $this->getObject()->getId(), 'required' => false)),
      'tipo_proyecto_id'                  => new sfValidatorPropelChoice(array('model' => 'TipoProyecto', 'column' => 'id')),
      'referencia'                        => new sfValidatorString(array('max_length' => 50, 'required' => false)),
      'productor_id'                      => new sfValidatorPropelChoice(array('model' => 'Productor', 'column' => 'id')),
      'ingresosanualesdeclarados'         => new sfValidatorInteger(array('min' => -2147483648, 'max' => 2147483647)),
      'costosanualesdeclarados'           => new sfValidatorInteger(array('min' => -2147483648, 'max' => 2147483647)),
      'estacionmayoresingresos'           => new sfValidatorString(array('max_length' => 45)),
      'estacionmayorescostos'             => new sfValidatorString(array('max_length' => 45)),
      'pregadicional1'                    => new sfValidatorBoolean(),
      'pregadicional2'                    => new sfValidatorBoolean(),
      'pregadicional3'                    => new sfValidatorBoolean(),
      'asociacionesproductores'           => new sfValidatorString(array('max_length' => 100, 'required' => false)),
      'garantiaspersonales'               => new sfValidatorBoolean(),
      'garantiasdeterceros'               => new sfValidatorBoolean(),
      'garantiasprendaohipoteca'          => new sfValidatorBoolean(),
      'garantiasdescuentos'               => new sfValidatorBoolean(),
      'garantiasotras'                    => new sfValidatorBoolean(),
      'firmada'                           => new sfValidatorInteger(array('min' => -2147483648, 'max' => 2147483647)),
      'oficina_id'                        => new sfValidatorPropelChoice(array('model' => 'Oficina', 'column' => 'id')),
      'estado'                            => new sfValidatorPropelChoice(array('model' => 'EstadoConsulta', 'column' => 'id')),
      'observaciones'                     => new sfValidatorString(array('required' => false)),
      'created_at'                        => new sfValidatorDateTime(array('required' => false)),
      'created_by'                        => new sfValidatorPropelChoice(array('model' => 'sfGuardUser', 'column' => 'id', 'required' => false)),
      'updated_at'                        => new sfValidatorDateTime(array('required' => false)),
      'updated_by'                        => new sfValidatorPropelChoice(array('model' => 'sfGuardUser', 'column' => 'id', 'required' => false)),
      'actividadconsultas_list'           => new sfValidatorPropelChoice(array('multiple' => true, 'model' => 'Actividad', 'required' => false)),
      'consulta_estaciones_costos_list'   => new sfValidatorPropelChoice(array('multiple' => true, 'model' => 'Estacion', 'required' => false)),
      'consulta_estaciones_ingresos_list' => new sfValidatorPropelChoice(array('multiple' => true, 'model' => 'Estacion', 'required' => false)),
    ));

    $this->widgetSchema->setNameFormat('consulta[%s]');

    $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema);

    parent::setup();
  }

  public function getModelName()
  {
    return 'Consulta';
  }

  public function updateDefaultsFromObject()
  {
    parent::updateDefaultsFromObject();

    if (isset($this->widgetSchema['actividadconsultas_list']))
    {
      $values = array();
      foreach ($this->object->getActividadconsultass() as $obj)
      {
        $values[] = $obj->getActividadId();
      }

      $this->setDefault('actividadconsultas_list', $values);
    }

    if (isset($this->widgetSchema['consulta_estaciones_costos_list']))
    {
      $values = array();
      foreach ($this->object->getConsultaEstacionesCostoss() as $obj)
      {
        $values[] = $obj->getEstacionId();
      }

      $this->setDefault('consulta_estaciones_costos_list', $values);
    }

    if (isset($this->widgetSchema['consulta_estaciones_ingresos_list']))
    {
      $values = array();
      foreach ($this->object->getConsultaEstacionesIngresoss() as $obj)
      {
        $values[] = $obj->getEstacionId();
      }

      $this->setDefault('consulta_estaciones_ingresos_list', $values);
    }

  }

  protected function doSave($con = null)
  {
    parent::doSave($con);

    $this->saveActividadconsultasList($con);
    $this->saveConsultaEstacionesCostosList($con);
    $this->saveConsultaEstacionesIngresosList($con);
  }

  public function saveActividadconsultasList($con = null)
  {
    if (!$this->isValid())
    {
      throw $this->getErrorSchema();
    }

    if (!isset($this->widgetSchema['actividadconsultas_list']))
    {
      // somebody has unset this widget
      return;
    }

    if (null === $con)
    {
      $con = $this->getConnection();
    }

    $c = new Criteria();
    $c->add(ActividadconsultasPeer::CONSULTA_ID, $this->object->getPrimaryKey());
    ActividadconsultasPeer::doDelete($c, $con);

    $values = $this->getValue('actividadconsultas_list');
    if (is_array($values))
    {
      foreach ($values as $value)
      {
        $obj = new Actividadconsultas();
        $obj->setConsultaId($this->object->getPrimaryKey());
        $obj->setActividadId($value);
        $obj->save($con);
      }
    }
  }

  public function saveConsultaEstacionesCostosList($con = null)
  {
    if (!$this->isValid())
    {
      throw $this->getErrorSchema();
    }

    if (!isset($this->widgetSchema['consulta_estaciones_costos_list']))
    {
      // somebody has unset this widget
      return;
    }

    if (null === $con)
    {
      $con = $this->getConnection();
    }

    $c = new Criteria();
    $c->add(ConsultaEstacionesCostosPeer::CONSULTA_ID, $this->object->getPrimaryKey());
    ConsultaEstacionesCostosPeer::doDelete($c, $con);

    $values = $this->getValue('consulta_estaciones_costos_list');
    if (is_array($values))
    {
      foreach ($values as $value)
      {
        $obj = new ConsultaEstacionesCostos();
        $obj->setConsultaId($this->object->getPrimaryKey());
        $obj->setEstacionId($value);
        $obj->save($con);
      }
    }
  }

  public function saveConsultaEstacionesIngresosList($con = null)
  {
    if (!$this->isValid())
    {
      throw $this->getErrorSchema();
    }

    if (!isset($this->widgetSchema['consulta_estaciones_ingresos_list']))
    {
      // somebody has unset this widget
      return;
    }

    if (null === $con)
    {
      $con = $this->getConnection();
    }

    $c = new Criteria();
    $c->add(ConsultaEstacionesIngresosPeer::CONSULTA_ID, $this->object->getPrimaryKey());
    ConsultaEstacionesIngresosPeer::doDelete($c, $con);

    $values = $this->getValue('consulta_estaciones_ingresos_list');
    if (is_array($values))
    {
      foreach ($values as $value)
      {
        $obj = new ConsultaEstacionesIngresos();
        $obj->setConsultaId($this->object->getPrimaryKey());
        $obj->setEstacionId($value);
        $obj->save($con);
      }
    }
  }

}


text:
"""
Copyright (c) 2013, Regents of the University of California
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

  * Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.

  * Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.

  * Neither the name of the University of California nor the names of its
    contributors may be used to endorse or promote products derived from this
    software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

from pyramid.config import Configurator
from pyramid_zodbconn import get_connection

from .models import appmaker
from .views import listen, publish, subscribe

# for some reason feedparser put this at the end and one of the
# other parsers catches our dates and doesn't consider DST
import feedparser
# re-registering this should put it at the front
feedparser.registerDateHandler(feedparser._parse_date_iso8601)

def root_factory(request):
    conn = get_connection(request)
    return appmaker(conn.root())

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    config = Configurator(root_factory=root_factory, settings=settings)

    config.add_static_view('static', 'static', cache_max_age=3600)

    config.add_route('publish', '/publish')
    config.add_view(publish, route_name='publish')

    config.add_route('subscribe', '/subscribe')
    config.add_view(subscribe, route_name='subscribe')

    config.add_route('listen', '/listen')
    config.add_view(listen, route_name='listen')

    return config.make_wsgi_app()


text:
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Concurrent;
using TrueCraft.API.Networking;
using System.Threading;
using TrueCraft.Core.Networking;
using System.Linq;
using TrueCraft.Core.Networking.Packets;
using TrueCraft.Client.Events;
using TrueCraft.Core.Logic;
using TrueCraft.API.Entities;
using TrueCraft.API;
using System.ComponentModel;
using System.IO;
using TrueCraft.Core;

namespace TrueCraft.Client
{
    public delegate void PacketHandler(IPacket packet, MultiplayerClient client);

    public class MultiplayerClient : IAABBEntity, INotifyPropertyChanged, IDisposable // TODO: Make IMultiplayerClient and so on
    {
        public event EventHandler<ChatMessageEventArgs> ChatMessage;
        public event EventHandler<ChunkEventArgs> ChunkModified;
        public event EventHandler<ChunkEventArgs> ChunkLoaded;
        public event EventHandler<ChunkEventArgs> ChunkUnloaded;
        public event PropertyChangedEventHandler PropertyChanged;

        private long connected;

        public TrueCraftUser User { get; set; }
        public ReadOnlyWorld World { get; private set; }
        public PhysicsEngine Physics { get; set; }
        public bool LoggedIn { get; internal set; }

        public bool Connected
        {
            get
            {
                return Interlocked.Read(ref connected) == 1;
            }
        }

        private TcpClient Client { get; set; }
        private IMinecraftStream Stream { get; set; }
        private PacketReader PacketReader { get; set; }

        private readonly PacketHandler[] PacketHandlers;

        private SemaphoreSlim sem = new SemaphoreSlim(1, 1);

        private readonly CancellationTokenSource cancel;

        private SocketAsyncEventArgsPool SocketPool { get; set; }

        public MultiplayerClient(TrueCraftUser user)
        {
            User = user;
            Client = new TcpClient();
            PacketReader = new PacketReader();
            PacketReader.RegisterCorePackets();
            PacketHandlers = new PacketHandler[0x100];
            Handlers.PacketHandlers.RegisterHandlers(this);
            World = new ReadOnlyWorld();
            var repo = new BlockRepository();
            repo.DiscoverBlockProviders();
            World.World.BlockRepository = repo;
            World.World.ChunkProvider = new EmptyGenerator();
            Physics = new PhysicsEngine(World, repo);
            SocketPool = new SocketAsyncEventArgsPool(100, 200, 65536);
            connected = 0;
            cancel = new CancellationTokenSource();
        }

        public void RegisterPacketHandler(byte packetId, PacketHandler handler)
        {
            PacketHandlers[packetId] = handler;
        }

        public void Connect(IPEndPoint endPoint)
        {
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += Connection_Completed;
            args.RemoteEndPoint = endPoint;

            if (!Client.Client.ConnectAsync(args))
                Connection_Completed(this, args);
        }

        private void Connection_Completed(object sender, SocketAsyncEventArgs e)
        {
            if (e.SocketError == SocketError.Success)
            {
                Interlocked.CompareExchange(ref connected, 1, 0);

                Physics.AddEntity(this);

                StartReceive();
                QueuePacket(new HandshakePacket(User.Username));
            }
            else
            {
                throw new Exception("Could not connect to server!");
            }
        }

        public void Disconnect()
        {
            if (!Connected)
                return;

            QueuePacket(new DisconnectPacket("Disconnecting"));

            Interlocked.CompareExchange(ref connected, 0, 1);
        }

        public void SendMessage(string message)
        {
            var parts = message.Split('\n');
            foreach (var part in parts)
                QueuePacket(new ChatMessagePacket(part));
        }

        public void QueuePacket(IPacket packet)
        {
            if (!Connected || (Client != null && !Client.Connected))
                return;

            using (MemoryStream writeStream = new MemoryStream())
            {
                using (MinecraftStream ms = new MinecraftStream(writeStream))
                {
                    ms.WriteUInt8(packet.ID);
                    packet.WritePacket(ms);
                }

                byte[] buffer = writeStream.ToArray();

                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                args.UserToken = packet;
                args.Completed += OperationCompleted;
                args.SetBuffer(buffer, 0, buffer.Length);

                if (Client != null && !Client.Client.SendAsync(args))
                    OperationCompleted(this, args);
            }
        }

        private void StartReceive()
        {
            SocketAsyncEventArgs args = SocketPool.Get();
            args.Completed += OperationCompleted;

            if (!Client.Client.ReceiveAsync(args))
                OperationCompleted(this, args);
        }

        private void OperationCompleted(object sender, SocketAsyncEventArgs e)
        {
            e.Completed -= OperationCompleted;

            switch (e.LastOperation)
            {
                case SocketAsyncOperation.Receive:
                    ProcessNetwork(e);

                    SocketPool.Add(e);
                    break;
                case SocketAsyncOperation.Send:
                    IPacket packet = e.UserToken as IPacket;

                    if (packet is DisconnectPacket)
                    {
                        Client.Client.Shutdown(SocketShutdown.Send);
                        Client.Close();

                        cancel.Cancel();
                    }

                    e.SetBuffer(null, 0, 0);
                    break;
            }
        }

        private void ProcessNetwork(SocketAsyncEventArgs e)
        {
            if (e.SocketError == SocketError.Success && e.BytesTransferred > 0)
            {
                SocketAsyncEventArgs newArgs = SocketPool.Get();
                newArgs.Completed += OperationCompleted;

                if (Client != null && !Client.Client.ReceiveAsync(newArgs))
                    OperationCompleted(this, newArgs);

                try
                {
                    sem.Wait(cancel.Token);
                }
                catch (OperationCanceledException)
                {
                    return;
                }

                var packets = PacketReader.ReadPackets(this, e.Buffer, e.Offset, e.BytesTransferred, false);

                foreach (IPacket packet in packets)
                {
                    if (PacketHandlers[packet.ID] != null)
                        PacketHandlers[packet.ID](packet, this);
                }

                if (sem != null)
                    sem.Release();
            }
            else
            {
                Disconnect();
            }
        }

        protected internal void OnChatMessage(ChatMessageEventArgs e)
        {
            if (ChatMessage != null) ChatMessage(this, e);
        }

        protected internal void OnChunkLoaded(ChunkEventArgs e)
        {
            if (ChunkLoaded != null) ChunkLoaded(this, e);
        }

        protected internal void OnChunkUnloaded(ChunkEventArgs e)
        {
            if (ChunkUnloaded != null) ChunkUnloaded(this, e);
        }

        protected internal void OnChunkModified(ChunkEventArgs e)
        {
            if (ChunkModified != null) ChunkModified(this, e);
        }

        #region IAABBEntity implementation

        public const double Width = 0.6;
        public const double Height = 1.62;
        public const double Depth = 0.6;

        public void TerrainCollision(Vector3 collisionPoint, Vector3 collisionDirection)
        {
            // This space intentionally left blank
        }

        public BoundingBox BoundingBox
        {
            get
            {
                return new BoundingBox(Position, Position + Size);
            }
        }

        public Size Size
        {
            get { return new Size(Width, Height, Depth); }
        }

        #endregion

        #region IPhysicsEntity implementation

        public bool BeginUpdate()
        {
            return true;
        }

        public void EndUpdate(Vector3 newPosition)
        {
            Position = newPosition;
        }

        public float Yaw { get; set; }
        public float Pitch { get; set; }

        internal Vector3 _Position;
        public Vector3 Position
        {
            get
            {
                return _Position;
            }
            set
            {
                if (_Position != value)
                {
                    QueuePacket(new PlayerPositionAndLookPacket(value.X, value.Y, value.Y + Height, value.Z, Yaw, Pitch, false));
                    if (PropertyChanged != null)
                        PropertyChanged(this, new PropertyChangedEventArgs("Position"));
                }
                _Position = value;
            }
        }

        public Vector3 Velocity { get; set; }

        public float AccelerationDueToGravity
        {
            get
            {
                return 0.08f;
            }
        }

        public float Drag
        {
            get
            {
                return 0.02f;
            }
        }

        public float TerminalVelocity
        {
            get
            {
                return 3.92f;
            }
        }

        #endregion

        public void Dispose()
        {
            Dispose(true);

            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                Disconnect();

                sem.Dispose();
            }

            sem = null;
        }

        ~MultiplayerClient()
        {
            Dispose(false);
        }
    }
}


text:
module Verifly
  # DependentCallbacks interface is similar to ActiveSupport::Callbacks, but it has few differences
  #
  # 1) `extend` it, not `include`
  #
  # 2) Use `.callback_groups` do tefine callbacks instead of define_callbacks
  #
  # 3) Better define callbacks in separate module, wich should extend DependentCallbacks::Storage
  #
  # 4) Use merge_callbacks_from(Module) instead of including it
  #
  # 5) There is no run_callbacks method.
  #
  # You can either use self.class.dependent_callbacks.invoke(group, *context) {}
  # or use .export_callbacks_to(:active_support) / .export_callbacks_to(:wrap_method)
  module DependentCallbacks
    autoload :Callback, "verifly/dependent_callbacks/callback"
    autoload :CallbackGroup, "verifly/dependent_callbacks/callback_group"
    autoload :Invoker, "verifly/dependent_callbacks/invoker"
    autoload :Service, "verifly/dependent_callbacks/service"
    autoload :Storage, "verifly/dependent_callbacks/storage"

    extend HasLogger
    include Storage

    # @api stdlib
    # Allows children to inherit callbacks from parent
    # @param [Class] child
    def inherited(child)
      super

      child.instance_exec(dependent_callbacks_service) do |service|
        @dependent_callbacks_service = Service.new(service)
      end
    end

    # Exports callbacks to another callback system / something like that
    # @param [:active_support, :wrap_method] target
    #   Target selection.
    #   * :active_support exports each group to correspoding
    #     ActiveSupport callback (via set_callback)
    #   * :wrap_method defines / redefines methods, named same as each group.
    #     If method was defined, on it's call callbacks would run around its previous defenition.
    #     If not, callbacks would run around nothing
    # @param groups [[Symbol]] arra of groups to export. Defaults to all groups
    def export_callbacks_to(target, groups: nil)
      (groups || dependent_callbacks_service.group_names).each do |group|
        case target
        when :active_support then _export_callback_group_to_active_support(group)
        when :action_controller then _export_callback_group_to_action_controller(group)
        when :wrap_method then _export_callback_group_to_method_wapper(group)
        else
          raise "#{target.inspect} export target unavailable. " \
                "available targets are :active_support, :action_controller, :wrap_method"
        end
      end
    end

    private

    # Exports callbacks to ActiveSupport
    # @see export_callbacks_to
    # @param group [Symbol] name of group
    def _export_callback_group_to_active_support(group)
      exports_name = :"__verifly_dependent_callbacks_exports_#{group}"

      define_method(exports_name) do |*context, &block|
        self.class.dependent_callbacks_service.invoke(group) do |invoker|
          invoker.context = context
          invoker.inner_block = block
          invoker.run(self)
        end
      end

      private(exports_name)
      set_callback(group, :around, exports_name)
    end

    # Exports callbacks to ActionController::Base
    # @see export_callbacks_to
    # @param group [Symbol] name of group
    def _export_callback_group_to_action_controller(group)
      raise unless group == :action

      around_action do |request, sequence|
        self.class.dependent_callbacks_service.invoke(group) do |invoker|
          invoker.context << request
          invoker.inner_block = sequence
          invoker.break_if { response_body }
          invoker.run(self)
        end
      end
    end

    # Exports callbacks to methods
    # @see export_callbacks_to
    # @param group [Symbol] name of group
    def _export_callback_group_to_method_wapper(group)
      instance_method = instance_method(group) rescue nil

      define_method(group) do |*args, &block|
        self.class.dependent_callbacks_service.invoke(group) do |invoker|
          invoker.around { instance_method&.bind(self)&.call(*args, &block) }
          invoker.context = args
          invoker.run(self)
        end
      end
    end
  end
end


text:
require "promise_pay"

PromisePay.api_user = "<%= email %>"
PromisePay.api_key  = "<%= token %>"

# Consider having this read from config
PromisePay.env      = :<%= env %>