Source: osi.js

/**
	@namespace osi
	@since 19/07/2020
	@version 1.0
	@author Brennan Wilkes
	@author 100322326
*/

//JSDOCS generation command
//jsdoc -d documentation/ js/island-generator/island.js js/osi.js js/island-generator/name_list.js

//-------------------------------------CONSTANTS AND GLOBALS------------------------------------------

/**
	List of concept art file names
	@type {string[]}
	@memberof osi
*/
var concept_art = [
	"Atalia-Nanai",
	"Hokulele-Kekoa",
	"Kainano-Taualai",
	"Kuilei-Inoke",
	"Murihau",
	"Rangi-Karauna",
	"Tamah-Talatonu",
	"Tama-Ropata",
	"Aru"
];

/**
	Variable to keep track of which page index is currently displayed
	@type {number}
	@memberof osi
*/
var currentPage = 1;

/**
	Array of the most recent visted pages. Used for "back" functionality
	@type {number}
	@memberof osi
*/
var pageHistory = new Array();

/**
	Storage for generated {@link Island} object
	@type {object}
	@memberof osi
*/
var island;

/**
	Homepage index enum
	@type {number}
	@memberof osi
	@constant
*/
const HOMEPAGE = 1;

/**
	Generator index enum
	@type {number}
	@memberof osi
	@constant
*/
const GENERATOR = 2;

/**
	Compiling index enum
	@type {number}
	@memberof osi
	@constant
*/
const COMPILING = 5;

/**
	Gallery index enum
	@type {number}
	@memberof osi
	@constant
*/
const GALLERY = 7;

/**
	Gallery preview image index enum
	@type {number}
	@memberof osi
	@constant
*/
const GALLERYPREVIEW = 8;

/**
	Documentation page index enum
	@type {number}
	@memberof osi
	@constant
*/
const DOCS = 9;

/**
	Dev notes index enum
	@type {number}
	@memberof osi
	@constant
*/
const DEVNOTES = 10;

/**
	Sources index enum
	@type {number}
	@memberof osi
	@constant
*/
const SOURCES = 11;

/**
	About index enum
	@type {number}
	@memberof osi
	@constant
*/
const ABOUT = 12;

/*
	https://stackoverflow.com/a/7768006

	Got this regex safari detector from stack overflow. Hope its ok.
	Apparently safari decides that if you apply any styling to an HTML5
	input type=color element, it just hides it completely!! What are those
	"geniuses" at apple thinking??? So much time wasted on this bug.

	I'm pretty well versed in regex, but only in the scope of using SED/GREP in unix world, and
	I don't have a clue what this is doing, so I've given up trying to write my own.
*/
/**
	Safari regex detector
	@type {boolean}
	@memberof osi
	@constant
*/
const SAFARI = (/^((?!chrome|android).)*safari/i.test(navigator.userAgent));

/**
	Detects if a user is using a mouse for navigation
	@type {boolean}
	@memberof osi
*/
var USING_MOUSE = false;


//------------------------------------GENERAL PURPOSE FUNCTIONS--------------------------------------


/**
	Sets an elements HTML5 validation message using a neat JS trick
	@param {string} id of element to search for
	@param {string} message to set
	@memberof osi
*/
function setValidMessage(id,message){
	let element = $("#"+id);
	element.attr("oninvalid","this.setCustomValidity('"+message+"')");
	element.attr("onchange","try{setCustomValidity('')}catch(e){}");
	element.attr("oninput","setCustomValidity(' ')");
}

/**
	Spawns a random particle to the screen with random coordinates and deletes it after two seconds
	@memberof osi
*/
function spawnParticle(){

	//Create particle div element
	let particle = $("<div class=Particle></div>");

	//Set random coordinates
	particle.css("left",Math.random()*window.innerWidth*0.95+"px");
	particle.css("top",Math.random()*window.innerHeight*0.95+"px");

	//Add particle to DOM
	$("body").prepend(particle)

	//Delete particle after two seconds
	setTimeout(function(e){
		particle.remove();
	}, 2000);
}

/**
	Changes directly to a requested page
	@param {number} page Page index to change to
	@memberof osi
*/
function changePage(page){

	if(page===undefined){

		//"back" mode. Set page to last visited page
		page = pageHistory.pop();
	}
	else{

		//Add current page to the stack
		stackPage();
	}

	//Exit current page
	$(".page:nth-child("+currentPage+")").fadeOut();

	//Update tracker
	currentPage = page;

	//Move to next page
	$(".page:nth-child("+currentPage+")").fadeIn();

	setTimeout(function(e){
		if(!USING_MOUSE){
			$(".page:nth-child("+currentPage+") :enabled:visible:first").focus();
		}
	},10);


}

/**
	Runs HTML5 form validation, then calls {@link changePage}
	@param {number} dir forward or backwards / direction to turn
	@memberof osi
*/
function turnPage(dir){

	//Check form validity
	if($("form")[0].reportValidity()){

		//Move forward/backward
		changePage(currentPage+dir);
	}
}

/**
	Add the current page to the history stack
	@memberof osi
*/
function stackPage(){

	//pushback
	pageHistory.push(currentPage);

	//Keep history size reasonable
	if(pageHistory.length > 3){
		pageHistory.shift();
	}
}

/**
	Grabs relavent information from the form, and generates an {@link Island} Updates {@link island} instead of returning
	@memberof osi
*/
function compileIsland(){

	//Generate settings object with defaults.
	let set;
	if($("#seed").val().length > 0){

		//Use custom seed
		set = new IslandSettings($("#seed").val());
	}
	else{
		set = new IslandSettings();
	}

	//Grab basic booleans
	let boolCheck = [
		["motu","HAS_MOTU"],
		["reef","HAS_REEF"],
		["atoll","IS_ATOLL"],
		["volcano","IS_VOLCANO"],
		["trees","HAS_TREES"],
		["background","colour_background"]
	];

	//Save checkbox states
	for(let i=0;i<boolCheck.length;i++){
		set[boolCheck[i][1]] = $("#"+boolCheck[i][0]).prop("checked");
	}

	//Save unique value scalers
	set.ISL_PERSIST = parseInt($("#persistence").val())/10;
	set.ISL_LAC = parseInt($("#lacunarity").val())/100;
	set.ISL_SCALE = parseInt($("#scale").val());

	set.HAS_TOWN = ($("#village").prop("checked") ? 0 : 1);
	set.village_size = parseInt($("#village_size").val());
	set.tree_amt = parseInt($("#tree_amt").val())*20;
	set.time = parseInt($("#time").val());

	//Grab basic values
	let valsCheck = [
		["name","name"],
		["ocean","DEEP_OCEAN"],
		["shallows","SHALLOW_OCEAN"],
		["ground1","LAND_ONE"],
		["ground2","LAND_TWO"],
		["ground3","LAND_THREE"],
		["beach","BEACH"],
		["rock1","ROCK_ONE"],
		["rock2","ROCK_TWO"],
		["lava1","LAVA_ONE"],
		["lava2","LAVA_TWO"]
	];

	//If set, save their contentes
	let tempLookup;
	for(let i=0;i<valsCheck.length;i++){
		tempLookup = $("#"+valsCheck[i][0]).val();
		if(tempLookup.length > 0){
			set[valsCheck[i][1]] = tempLookup;
		}
	}

	//Generate island object
	island = new Island(set);

	//Try to generate PNG data
	try{
		$("#preview_display").prop("src",island.compileStaticImage(true,true));
	}
	catch(e) {

		//Offline file mode warning message
		alert("Island images cannot be generated with trees and villages when the page is being loaded from a local file. This is due to browser security messures. Either generate without trees and villages, or checkout the deployment at https://brennanwilkes.github.io/Open-Source-Islands/")
		return false;
	}
	return true;
}

/**
	Main setup function. Prepares the DOM
	@memberof osi
*/
function main(){

	//Set validation messages for regex checkers
	setValidMessage("name","Island names may only contain letters, spaces hyphens, and apostrophes")
	setValidMessage("seed","Island Seeds may only contain digits");

	//Set up random background art
	let bgkimg = concept_art[Math.floor(Math.random()*concept_art.length)];
	$("#backgroundDisplay .lighting").css("background-image","url('concept-art/"+bgkimg+"-lighting.png')")
	$("#backgroundDisplay .baselayer").css("background-image","url('concept-art/"+bgkimg+".png')")

	//Set background page for smoother transitions
	$("form").append("<div class=page id=backgroundPage></div>");

	//Create temp image holders
	$("<img src=\"\" alt=\"Island image preview\">").insertAfter("#gallery-preview h2");
	$("<img id=preview_display src=\"\" alt=\"Island image preview\">").insertAfter("#preview-display-wrapper h2");

	//IOS mode - Adjust element heights to match IOS screen size. Damn you apple!!! >:(
	if(Math.abs($("body").height() - window.innerHeight) > 1){
		$("body").height(window.innerHeight);
		$("body").width(window.innerWidth);
		$(".page").css("max-width",window.innerHeight * 0.675);
		$(".page").width(window.innerWidth * 0.9);
		$(".page").height(window.innerWidth * 1.2);
		$(".page").css("max-height",window.innerHeight * 0.9);
	}

	//Trigger particle spawning
	setInterval(spawnParticle, 55);

	//Randomly set column and row span for gallery images
	let galimgs = $("#gal div").children();
	let ran;
	let threes = 0;
	let last = false;
	for(let i=0;i<galimgs.length;i++){
		ran = Math.random();

		//Set to 3x3 tile - <35% chance due to previous sizes
		if(ran < 0.35 && (i-threes)%3 === 0 && !last && i > 3){
			$(galimgs[i]).css("grid-column","auto / span 3");
			$(galimgs[i]).css("grid-row","auto / span 3");
			threes++;
			last = true;
		}

		//Set to 2x2 tile - <65% chance
		else if(ran < 0.65 &&(i-threes)%3 != 2){
			$(galimgs[i]).css("grid-column","auto / span 2");
			$(galimgs[i]).css("grid-row","auto / span 2");
			i+=2;
			last = false;
		}
		else{
			last = false;
		}
	}

	//Set atoll requirement autoclicking
	$("#atoll").change(function() {
		if(this.checked){
			$("#volcano").prop("checked", false);
			$("#motu").prop("checked", true);
		}
	});

	//Set volcano discrepancy autoclicking
	$("#volcano").change(function() {
		if(this.checked){
			$("#atoll").prop("checked", false);
		}
	});

	//Set motu discrepancy autoclicking
	$("#motu").change(function() {
		if(!this.checked && $("#atoll")[0].checked){
			$("#motu").prop("checked", true);
		}
	});

	//Set toggleable input events
	let slidertoggle = [["village","village_size"],["trees","tree_amt"],["volcano","lava1"],["background","ocean"]];
	for(let i=0;i<slidertoggle.length;i++){
		$("#"+slidertoggle[i][0]).change(function() {
			$("#"+slidertoggle[i][1]).parent()[this.checked ? "show" : "hide"]();
		});
	}

	//Set up button click events
	setUpButtonClicks();

	//Add extra form data on submission events
	$("form").submit(function(e){
		$("<input />")
			.attr("type", "hidden")
			.attr("name", "imageData")
			.attr("value", $("#preview_display").attr("src"))
			.appendTo(this);
		return true;
	});

	//Detect mouse click
	$("*").click(function(e){
		USING_MOUSE = true;
	});

	//Focus title text to start application
	$("#osi").focus();

	//Add non-safari-safe css styling
	if(!SAFARI){
		$("input[type=color]").css("border","0").css("width","40%").css("height","90%");
	}
}

/**
	Builds the button click events that power the sitemap navigation
	@memberof osi
*/
function setUpButtonClicks(){

	//Basic link buttons
	let buttonMap = [
		["#documentation",DOCS],
		["#about",ABOUT],
		["#devnotes",DEVNOTES],
		["#sources",SOURCES],
		["input[value=back]",undefined],
		[".home",HOMEPAGE],
		["#generator",GENERATOR],
		["#edit",GENERATOR]
	];

	//Listeners
	for(let i=0;i<buttonMap.length;i++){
		$(buttonMap[i][0]).click(function(e){
			changePage(buttonMap[i][1]);
		});
	}

	//Special case blur animation
	$("input[type=button], input[type=submit], button").click(function(e){
		$(this).blur();
	});

	//Turn to next page with form validation
	$("input[value=next]").click(function(e){
		turnPage(1);
	});

	//Gallery image click event
	$("#gal div").children().click(imageClickEvent).keypress(function(e){
		if(e.which == 13){
			$(this).click();
		}
	});

	//Button function events
	let complexEvents = [
		["#compile, #recompile",compileEvent],
		["#save",saveEvent],
		["#gallery",galleryEvent],
		["#copy",copyEvent]
	];

	for(let i=0;i<complexEvents.length;i++){
		$(complexEvents[i][0]).click(complexEvents[i][1]);
	}
}

/**
	Event handler for image gallery image clicks
	@param {object} e Event
	@memberof osi
*/
function imageClickEvent(e){

	//Change to preview page
	changePage(GALLERYPREVIEW);

	//Set src attribute
	$("#gallery-preview img").attr("src", $(this).attr("src"));

	//Save id as alt text
	$("#gallery-preview img").attr("alt", this.id);

	//Set name text
	getIslandData("name",parseInt(this.id),function(name){
		$("#gallery-preview h2")[0].innerHTML = name;
	});
}

/**
	Gets a specific island param from the database of island with specific idea
	@param {number} id ID to query
	@param {string} param Param to query for
	@param {function} behaviour Special behaviour to run. If left undefined, value will be returned
	@returns {string} data from database
	@memberof osi
*/
function getIslandData(param,id, behaviour){
	var temp;
	$.ajax({
		url: "php/ajax.php",
		method: "POST",
		async: (behaviour!=undefined),
		data: {
			id: id,
			request: param
		}}).done(function(data){
			if(behaviour!=undefined){
				behaviour(data);
			}
			else{
				temp = data;
			}
		}).fail(function(jqXHR,status){
			alert("failed! "+jqXHR+status);
		});
	return temp;
}

/**
	"Edit" clone of image in gallery event
	@param {object} e Event
	@memberof osi
*/
function copyEvent(e){

	changePage(COMPILING);

	//wait 600ms for animation to start
	setTimeout(function(){

		//jQuery request
		let img = $("#gallery-preview img");
		let imgID = img.attr("alt");


		//Attributes to copy
		let valsSet = [
			["seed","seed"],
			["name","name"],
			["ocean","deep_ocean"],
			["shallows","shallow_ocean"],
			["ground1","land_one"],
			["ground2","land_two"],
			["ground3","land_three"],
			["beach","beach"],
			["rock1","rock_one"],
			["rock2","rock_two"],
			["lava1","lava_one"],
			["lava2","lava_two"]
		];

		//Copy into form fields
		for(let i=0;i<valsSet.length;i++){
			$("#"+valsSet[i][0]).val(getIslandData(valsSet[i][1],imgID));
		}

		//Boolean attributes
		let boolsSet = [
			["motu","has_motu"],
			["reef","has_reef"],
			["volcano","is_volcano"],
			["atoll","is_atoll"],
			["village","has_town"],
			["trees","has_trees"],
			["colour_background","colour_background"]
		];

		//Copy state into checkboxes
		for(let i=0;i<boolsSet.length;i++){
			$("#"+boolsSet[i][0]).prop("checked",getIslandData(boolsSet[i][1],imgID)==="1");
		}

		//Special case attribute copy
		$("#time").val(parseInt(getIslandData("sunset",imgID)))
		$("#tree_amt").val(parseInt(getIslandData("tree_amt",imgID)))
		$("#village_size").val(parseInt(getIslandData("village_size",imgID)))
		$("#persistence").val(parseFloat(getIslandData("isl_persist",imgID)))
		$("#lacunarity").val(parseFloat(getIslandData("isl_lac",imgID)))
		$("#scale").val(parseFloat(getIslandData("isl_scale",imgID)))

		//Change to generator page
		changePage(GENERATOR);
	},600);

}

/**
	Event handler for entering image gallery
	@param {object} e Event
	@memberof osi
*/
function galleryEvent(e){

	//Navigation
	changePage(GALLERY);

	//Scroll the page down
	let div = $("#gal div");
	div.scrollTop(div.height());
	let amt = div.height();

	//Slowly scroll page back to top
	let scrollTimer = setInterval(function(){
		amt -= div.height()/50;
		if(amt<=0){
			div[0].scrollTo({top: 0});
			clearInterval(scrollTimer);
		}
		else{
			div[0].scrollTo({top: amt})
		}
	},5);
}

/**
	Event handler for saving an image. See {@link Island}
	@param {object} e Event
	@memberof osi
*/
function saveEvent(e){
	island.saveImage($("#village").prop("checked"),true);
}

/**
	Event handler for compiling island
	@param {object} e Event
	@memberof osi
*/
function compileEvent(e){

	//Recompile or compile normal navigation. Navigates to compile screen
	changePage(COMPILING);

	//Resets seed and name on recompile
	if(this.id==="recompile"){
		$("#seed").val("");
		$("#name").val("");
	}

	//Wait 600ms for animations to start. Without this, island compilation freezes keyframes
	setTimeout(function(){

		//Compile island
		if(compileIsland()){

			//On success, save seed and name and proceed to preview screen
			turnPage(1);
			$("#seed").val(island.replicable_seed);
			$("#name").val(island.name);
		}
		else{

			//On failure, return to generator screen
			changePage(GENERATOR);
		}
	},600);
}

//Run main function on document load
$(document).ready(main);