// Bob Hanson hansonr@stolaf.edu


// getWyckoff.spt
//
// 2023.10.24 Bob Hanson hansonr@stolaf.edu
//
// requires Jmol 16.1.43 or higher
//
// Create JSON of International Tables space group data, 
// including all settings found using the getgen program 
// for the settings (using &settings=ITA+Settings) 
// and for general positions (using &what=text and mat=-y,-z,x-y)
//
// For generators and Wyckoff positions, we use trgen
// with flags what=gen and what=wpos. &trmat=c,-a-c,b is used rather than &trm=-y,-z,x-y 
//
// for example:
// 
// https://www.cryst.ehu.es/cgi-bin/cryst/programs/nph-getgen?gnum=7&what=text&mat=x,y,z
// https://www.cryst.ehu.es/cgi-bin/cryst/programs/nph-getgen?gnum=7&settings=ITA+Settings
// https://www.cryst.ehu.es/cgi-bin/cryst/programs//nph-trgen?gnum=51&what=gen&trmat=b,a,-c
//
// settings included are those from getgen, not wp-list:
//
// https://www.cryst.ehu.es/cgi-bin/cryst/programs/nph-wp-list?gnum=7&settings=ITA+Settings
// 
// which are far more abundant. Note that with just a bit of modification, this script could be used to 
// create files from any desired list of transfomrmations. For now I a just using this simple list.
//
// General position coordinates for Wyckoff positions are skipped, as they are redundant.
//
// Output files are available in the src/org/jmol/symmetry/sg/json folder
//

// a bit of hardwiring:

targetDir = "c:/temp/bilbao/wyckoff/";

// variables for the createJSONFiles call (below), after the functions

itadelay = 0.1;
itaFirst = 0;
itaForceNew = false;

// only one function to run here:

function createJSONFiles(n, forceNew) {

	// creates ita_<i>.json, by space group

	// n option to start at a given number (-30), or only a certain number (30), or all (0)
	// forceNew option true to overwrite local files; false to use them if they are there.

	var allSettings = []; 
	var allOps = [];
	for (var i = 1; i <= 230; i++) {
		if (n < 0 && i < -n || n > 0 && i != n) {
			continue;
		}
		_getAllOps(i, allOps);
		var adata = _createJSONArray(i, forceNew, allSettings);
		var thedata = adata.format("JSON");
		var fname = targetDir + "json/ita_" + i + ".json";
		write var thedata @fname;
	}
	if (n == 0 || n == -1) {
		var thedata = allSettings.format("JSON");
		var fname = targetDir + "json/ita_all.json";
		write var thedata @fname;
		var thedata = allOps.format("JSON");
		var fname = targetDir + "json/ops_all.json";
		write var thedata @fname;
		print "done -- " + allSettings.length + " settings and " + allOps.length + " operations";
	}
}

function _getAllOps(sg, allOps) {
	var y = _getITADataHTM(sg, 1, "gp_full", "", forceNew).split('<td align="center"');
	for (var i = 8; i < y.length; i += 4) {
		var xyz = y[i].split("&nbsp;")[3].trim();
		if (!xyz) {
			i -= 3;
			continue;
		}
		var m = y[i].split("<pre>")[2].split("</pre>")[1].trim().replace("\n"," |");
		var ita = _fixName((y[i+1])[2][0].split("</td")[1].trim());
		var seitz = _fixName((y[i+2])[2][0].split("</td")[1].trim().replace("<sub>","_<sub>"));
		allOps.push({"sg":i,"xyz":xyz,"ita":ita,"seitz":seitz,"m":m});
	}
}

// all other functions are "private" to this script

function _createJSONArray(i, forceNew, allSettings) {

	// Create ITA records for a specific space group.

	// start by getting the ITA settings using getgen 
	// some reordering is done for groups 48, 50, 59, 68, and 78,
	// which for some reason are not presented with the default (a,b,c) transformation
	// as the first setting on the list. 

	var x = _getITADataHTM(i, 0, "", "", forceNew);
	var x = x.split("<a href=")[4][-3];
	var sg = {};
	var k0 = 0;
	sg.sg = i;
	sg.its = [];
	sg.n = x.length;
	var outOfOrder = false;
	for (var j = 1; j <= x.length; j++) {
  		var its = {};  
		var a = x[j].split('"');
		var href = "https://www.cryst.ehu.es" + a[2].replace("//","/");
		if (href.find("unconv=")) {
			its.u =  _fixName(_getField(href, "unconv", 100));
		} else if (!href.find("Standard")) {
			href += "&standard=Standard%2FDefault+Setting";
		}
		if (!its.u) {
			its.u = _fixName((a[3])[2][0].split("</a>")[1]);
		}
		its.id = _fixName(its.u.split("[")[1].split("(")[1]).replace(" ","");
		var tm = _getField(href, "trmat", 100);	
		if (!tm)tm = "a,b,c";		
		its.tm = tm;
		its.set = j;
		its.sg = i;
		if (tm == "a,b,c" && j > 1 || outOfOrder) {
			// out of order! -- we want origin 2 to be first here because it is the default
			// space groups 48, 50, 59, 68, and 70
			if (!outOfOrder) {
				for (var k = sg.its.length; k > 0; --k) {
					var kk = k + x.length/2;
					var itk = sg.its[k];
					sg.its[kk] = itk;
				}
			}
			k0++;
			sg.its[k0] = its;
			outOfOrder = true;
		} else {
	  		sg.its.push(its);
		}

		var data = _getGPText(i, j, its.tm, forceNew);
		its.gp = _getGPJson(its, data);
		data = _getITADataHTM(i, j, "gen", its.tm, forceNew);
		its.gen = _getGeneratorJson(data);
		data = _getITADataHTM(i, j, "wpos", its.tm, forceNew);
		its.wpos = _getWyckoffJson(its, data);
	}
	for (var j = 1; j <= sg.its.length; j++) {
		sg.its[j].set = j;
		allSettings.push(sg.its[j]);
	}
	return sg;
}

function _getITADataHTM(i, j, what, trmat, forceNew) {

	// the main function for retieving data from trgen or getgen

	var localFile = targetDir + "data/" + if(what ; what ; "its") + "_" + i + if(j;"_"+j+".html";".htm");
	var prog = if(trmat ; "trgen" ; "getgen");
	var url = if(forceNew ; "https://www.cryst.ehu.es/cgi-bin/cryst/programs/nph-" + prog 
		+ "?gnum=" + i 
		+ if(what ; "&what=" + what.split("_")[1] ; "&settings=ITA+Settings")
		+ if(trmat ; "&trmat=" + trmat ; "")
		;  localFile);
	print "getting " + url;
	delay @itadelay;
	var data = load(url);
	if (forceNew) {
		write var data @localFile;
	} else if (data.find("FileNotFound")) {
		data = _getITADataHTM(i, j, what, trmat, true);
	}
	return data;
}

function _getGPText(i, j, trmat, forceNew) {

	// specifically for retieving data from getgen for general position text

	var localFile = targetDir + "data/gp_" + i + if(j;"_"+j+".html";".htm");
	var url = if(forceNew ; "https://www.cryst.ehu.es/cgi-bin/cryst/programs/nph-getgen" 
		+ "?gnum=" + i + "&what=text&mat=" + _toXYZ(trmat)
		;  localFile);
	print "getting " + url;
	delay @itadelay;
	var data = load(url);
	if (forceNew) {
		write var data @localFile;
	} else if (data.find("FileNotFound")) {
		data = _getGPText(i, j, trmat, true);
	}
	return data;
}

function _getGPJson(its, data) {

	// just split the text on the <big> tag

	var gp = [];  
	var d = data.split("<big>");
	for (var j = 2; j <= d.length; j++) {
		gp.push(d[j].split(" ")[2].split("<")[1]);
  	}
	var sg = spacegroup(gp.join(";"));
	if (sg && sg.type == "hash") {
		its.itaFull = sg.itaFull;
	} else {
		its.itaFull = "ita?" + i + "." + isetting;
	}
	return gp;
}

function _getGeneratorJson(data) {

	// parse the generator list using <nobr> tags. All cases return two columns here, 
	// so we always just skip the first column.

	var gen = [];
	var y = data.split("<nobr>");
	for (var j = 5; j <= y.length; j += 3) {
		var coor = y[j++].split("</nobr>")[1] + "," + y[j++].split("</nobr>")[1] + "," + y[j++].split("</nobr>")[1];
		gen.push(coor);
  	}
	return gen;
}

function _getWyckoffJson(its, data) {

	// Wyckoff positions are a bit trickier.
	// currently adding some Jmol-derived geometric elements here, just for the first member of the list.

	var wyck = {};  
	data = data.split("NOTE")[1];
	var isUnconv = data.find("Standard/Default setting");
	var w = data.split("Wyck");
	var y = w[4].split("align=center>")[2][0];
	var pos = [];
	wyck.pos = pos;
	var n = 0;
	for (var j = 1; j <= y.length; j++) {
		if (j == 1 && y[j].find("(")) {
			if (isUnconv) {
				// skip first column
				j++;
			}
			var c = y[j].split("</td")[1].split("+");
			wyck.cent = [];
			for (var k = 1; k < c.length; k++) {
				wyck.cent.push(c[k].split("(")[2].split(")")[1]);
			}
			continue;
		}
		var d = {};
		d.mult = 0+y[j++].split("</td")[1];
		d.label = y[j++].split("</td")[1];
		var td = y[j];
		var it = td.find("<");
		d.desc = td[1][it - 1];
		if (!td.find("(")) {
			td = y[++j];
		}
		td = td.split("<nobr>");
		var coor = [];
		for (var k = 2; k <= td.length; k++) {
			var t = td[k];
			if (t.find('(') == 1) {
				t = '">' + t;
			}
			if (t.find('">') && t.find(')')) {
				var p = t.split('">')[2].split("(")[2].split(")")[1];
				coor.push(p);
			}
		}
		if (++n == 1) {
			// skip first, which is general positions in a transformed format
			d.geom = "general";
		} else {
			d.coord = coor;
			d.geom = _getWyckoffElement(coor[1]);
		}
		pos.push(d);
  	}
	return wyck;
}


function _getWyckoffElement(p) {
	var xyz = p.split(",");
	var n = 0; 
	var nxyz = ({});
	for (var i = 1; i <= 3; i++) {
		if (xyz[i].find('x')) {
			nxyz |= 1;
		}		
		if (xyz[i].find('y')) {
			nxyz |= 2;
		}		
		if (xyz[i].find('z')) {
			nxyz |= 3;
		}		
	}
	switch (nxyz.length) {
	case 0:
		return "point";
	case 1:
		return "line";
	case 2:
		return "plane";
	}
	return "general";
}

// utility methods

function _fixName(s) {

	// remove HTML markings

	return (s.trim()
		.replace("<i>","").replace("</i>","")
		.replace("<b>","").replace("</b>","")
		.replace("<sub>","").replace("</sub>","")
		.replace("&#124;","|"));
}

function _toXYZ(abc){

	// getGen needs the transposed matrix, in xyz format. 
	// this does the trick, as -m4 is the transpose of m4, and symop(matrix, "xyz") 
 	// will accept a,b,c and return x,y,z
	// check for translation and don't transpose that

	var a = symop(abc, "matrix");
	var t = a%2; // get translation vector
	if (1.0 * t == 0) {
		// transpose if no translation
		a = -a;
	} else {
		// remove translation from 4x4, transpose it, then return translation
		a = -(a + -t) + t;
  }
  return symop(a, "xyz");
}

function _getField(data, field, max) {
	
	// get a &... field form a URL

	var i = data.find(field + "=");
	return (i == 0 ? "" : (data[i+field.length + 1][i+max] + "&").split("&")[1]);
}

// run the function

createJSONFiles(itaFirst, itaForceNew);


/**

Lots missing, but no first setting at least.
First one missing is #75 (tetragonols) and last is #167 (trigonals, :r option never implemented in Jmol). 