/******************************************************************
 *          fichier schoolkit.js
 ******************************************************************
 *
 * Ce programme permet de placer à l'écran divers ustensiles géométriques
 * semi-transparents, avec des poignées pour le déplacer, en
 * translation et en rotation
 *
 * Copyright : 2021 Georges Khaznadar
 * Licence   : GPL version 3 ou supérieure.
 *
 * Vous êtes libre de copier, d'utiliser, de distribuer ce programme,
 * tel quel ou sous une forme modifiée, moyennent quelques
 * restrictions, décrites à :
 * https://www.april.org/files/gfdl.1.3-js.fr.html
 *
 * Voir aussi le fichier d'exemple index.html, pour une
 * utilisation élémentaire de divers ustensiles.
 *
 ******************************************************************/

/**
 * rend un groupe SVG draggable pour les translations
 * envoie des évènements "maison" `dragstart`, `drag`, and `dragend`
 * avec une propriété `data` qui contient les coordonnées x, y dans 
 * l'espace SVG, ou dx, dy (déplacement depuis le début) pour le drag.
 *
 * inspiré de https://riptutorial.com/svg/example/17731/dragging-svg-elements
 *
 * @param g un groupe <g> SVG
 * @param poignee un fils de `g` qui sera utilisé comme poignée pour
 *               initier le déplacement.
 * @param centre le point de rotation du groupe
 * @param cursor une chaîne de caractère pour le curseur (CSS)
 * @param startsignal nom du signal à créer quand le déplacement commence
 * @param movesignal  nom du signal à créer durant le déplacement
 * @param endsignal   nom du signal à créer à la fin du déplacement
 **/
function draggable(g, poignee, centre, cursor,
		   startsignal, movesignal, endsignal){
    var svg = g;
    while (svg && svg.tagName!='svg') svg=svg.parentNode;
    var pt=svg.createSVGPoint(), doc=svg.ownerDocument;

    var root = doc.rootElement || doc.body || svg;
    var xlate, matrix_start, mouseStart;
    var bv = g.transform.baseVal;
    var data = new DOMPoint();

    poignee.addEventListener('mousedown',startMove,false);
    poignee.style.cursor = cursor;
    
    function startMove(evt){
	// We listen for mousemove/up on the root-most
	// element in case the mouse is not over g.
	root.addEventListener('mousemove',handleMove,false);
	root.addEventListener('mouseup',  finishMove,false);

	// Ensure that the first transform is at least an identity matrix
	if (!bv.numberOfItems>0 ){
	    bv.initialize(svg.createSVGTransformFromMatrix(svg.createSVGMatrix()));
	}
	matrix_start = bv.consolidate().matrix;
	mouseStart = inElementSpace(evt);
	data = inElementSpace(evt);
	fireEvent(startsignal);
    }

    function handleMove(evt){
	var point = inElementSpace(evt);
	data.x = point.x - mouseStart.x; data.y = point.y - mouseStart.y;
	fireEvent(movesignal);
    }

    function finishMove(evt){
	root.removeEventListener('mousemove',handleMove,false);
	root.removeEventListener('mouseup',  finishMove,false);
	data = inElementSpace(evt);
	fireEvent(endsignal);
    }

    function fireEvent(eventName){
	var event = new Event(eventName);
	event.data = data;
	return g.dispatchEvent(event);
    }

    // Convert mouse position from screen space to coordinates of g
    function inElementSpace(evt){
	pt.x=evt.clientX; pt.y=evt.clientY;
	return pt.matrixTransform(g.parentNode.getScreenCTM().inverse());
    }
}


/**
 * ajoute un élément PATH à un élément SVG
 *
 * @param svg l'élément racine SVG
 * @param d la formule du PATH
 * @param attr un objet contenant tous les attributs, dont le style.
 * @return l'élément PATH nouveau
 **/
function create_path(svg, d, attr){
    let path =  document.createElementNS('http://www.w3.org/2000/svg', 'path');
    if (attr !== "undefined"){
	for (a in attr){
	    path.setAttribute(a, attr[a]);
	}
    }
    path.setAttribute("d", d);
    svg.append(path);
    return path;
}

/**
 * ajoute un élément RECT à un élément SVG
 *
 * @param svg l'élément racine SVG
 * @param rect un objet avec des propriétés x, y, w, h
 * @param attr un objet contenant tous les attributs, dont le style.
 * @return l'élément RECT nouveau
 **/
function create_rect(svg, rect, attr){
    let rectangle =  document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    if (attr !== "undefined"){
	for (a in attr){
	    rectangle.setAttribute(a, attr[a]);
	}
    }
    for (prop in rect){
	rectangle.setAttribute(prop, rect[prop])
    }
    svg.append(rectangle);
    return rectangle;
}

/**
 * ajoute un élément CIRCLE à un élément SVG
 *
 * @param svg l'élément racine SVG
 * @param center un objet avec des propriétés x, y
 * @param radius le rayon
 * @param attr un objet contenant tous les attributs, dont le style.
 * @return l'élément CIRCLE nouveau
 **/
function create_circle(svg, center, radius, attr){
    let circle =  document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    if (attr !== "undefined"){
	for (a in attr){
	    circle.setAttribute(a, attr[a]);
	}
    }
    circle.setAttribute("cx", center.x);
    circle.setAttribute("cy", center.y);
    circle.setAttribute("r", radius);
    svg.append(circle);
    return circle;
}

/**
 * ajoute un élément TEXT à un élément SVG
 *
 * @param svg l'élément racine SVG
 * @param point un objet avec des propriétés x, y
 * @param text le texte
 * @param attr un objet contenant tous les attributs, dont le style.
 * @return l'élément TEXT nouveau
 **/
function create_text(svg, point, text, attr){
    let txt =  document.createElementNS('http://www.w3.org/2000/svg', 'text');
    if (attr !== "undefined"){
	for (a in attr){
	    txt.setAttribute(a, attr[a]);
	}
    }
    txt.setAttribute("x", point.x);
    txt.setAttribute("y", point.y);
    txt.appendChild(document.createTextNode(text));
    svg.append(txt);
    return txt;
}

/**
 * Constructeur pour la classe GeomPaper
 * @param ident l'identifiant d'un conteneur, par exemple un d'élément <div>
 * @param canvasWidth largeur de l'objet SVG qui sera créé
 * @param canvasHeight hauteur de l'objet SVG qui sera créé
 **/
class GeomPaper {

    constructor(ident, canvasWidth, canvasHeight){
	this.container = document.getElementById(ident);
	this.container.style.width = canvasWidth+"px";
	this.width = canvasWidth;
	this.height = canvasHeight;
	this.svgRoot = document.createElementNS(
	    'http://www.w3.org/2000/svg', 'svg');
	this.svgRoot.setAttribute(
	    'viewBox', '0 0 ' + canvasWidth + ' ' + canvasHeight);
	let container = document.getElementById(ident);
	container.append(this.svgRoot);
	this.tools = [];
	return this;
    }

    /**
     * change d'échelle chaque outil enregistré
     * @param s l'échelle
     **/
    rescale(s){
	this.tools.forEach((obj) => obj.scale(s));
    }
    
    /**
     * dessine un quadrillage dans le fond
     **/
    quadrillage(){
	let ligne_fine = {
	    "stroke-width": 0.05,
	    "stroke": "black",
	    "style": "z-index: -10;",
	};
	let ligne_epaisse = {
	    "stroke-width": 0.3,
	    "stroke": "black",
	    "style": "z-index: -10;",
	};
	let d = "";
	for(let i=0; i< this.width; i += 10){
	    d += "M " + i + " 0 V "+ this.height + " ";
	}
	create_path(this.svgRoot, d, ligne_fine);

	
	d = ""
	for(let i=0; i< this.width; i += 100){
	    d += "M " + i + " 0 V "+ this.height + " ";
	}
	create_path(this.svgRoot, d, ligne_epaisse);

	d = "";
	for(let i=0; i< this.width; i += 10){
	    d += "M 0 " + i + " H "+ this.width + " ";
	}
	create_path(this.svgRoot, d, ligne_fine);

	d = "";
	for(let i=0; i< this.width; i += 100){
	    d += "M 0 " + i + " H "+ this.width + " ";
	}
	create_path(this.svgRoot, d, ligne_epaisse);
    }

    /**
     * Ajoute un outil graphique
     *
     * @param aTool un outil graphique, descendant de GeomTool
     * @param center un point où centrer l'outil (facultatif)
     * @param scale une échelle pour tracer l'outil initialement (facultative)
     **/
    addTool(aTool, center, scale){
	this.tools.push(aTool);
	aTool.parent = this;
	if (typeof(center) !== "undefined"){
	    aTool.translate(
		center.x - aTool.centre.x,
		center.y - aTool.centre.y
	    );
	}
	if (typeof(scale) !== "undefined") aTool.scale(scale);
	return aTool;
    }
}

/**
 * Constructeur pour la classe GeomTool
 * @param ident l'identifiant d'un conteneur, par exemple un d'élément <div>
 * @param canvasWidth largeur de l'objet SVG qui sera créé
 * @param canvasHeight hauteur de l'objet SVG qui sera créé
 * @param scale échelle de l'objet qui sera contenu dans le canevas
 **/
class GeomTool {

    /**
    * Le constructeur
    * @param paper une instance de GeomPaper
    * @param cx abscisse du centre de rotation
    * @param cy ordonnée du centre de rotation
    * @param ident un identifiant, optionnel
    **/
    constructor (paper, cx, cy, ident){
	this.own_scale = 1;
	this.own_heading = 0;
	this.svg = paper.svgRoot;
	this.centre = new DOMPoint(cx,cy);
	if (typeof(ident) !== "undefined"){
	    this.ident=ident;
	} else {
	    this.ident= "id" + Math.floor(Math.random() * 1000000);
	}
	this.group = document.createElementNS('http://www.w3.org/2000/svg','g');
    }

    /**
     * prend en charge la construction du groupe et son affichage
     */
    registerGroup(){
	this.group.setAttribute("id", this.ident);
	this.group.transform.baseVal.initialize(
	    this.svg.createSVGTransformFromMatrix(this.svg.createSVGMatrix()));
	this.svg.append(this.group);
	this.centre_zero = this.create_circle(
	    this.centre,
	    5,
	    {
		fill: "cyan",
		opacity: 0.001,
	    }
	);
	this.dessine();
	this.translatable();
	this.rotatable();
    }
    
    /**
     * Initialise les dessins propres à l'objet géométrique
     **/
    dessine() {
	throw("Erreur : il ne faut pas instancier directement la classe GeomOutil");
    }

    /**
     * ajoute un élément PATH dans this.group
     *
     * @param d la formule du PATH
     * @param attr un objet contenant tous les attributs, dont le style.
     * @return l'élément PATH nouveau
     **/
    create_path(d, attr){
	return create_path(this.group, d, attr);
    }

    /**
     * ajoute un élément RECT à this.group
     *
     * @param rect un objet avec des propriétés x, y, w, h
     * @param attr un objet contenant tous les attributs, dont le style.
     * @return l'élément RECT nouveau
     **/
    create_rect(rect, attr){
	return create_rect(this.group, rect, attr);
    }
    
    /**
     * ajoute un élément CIRCLE à this.group
     *
     * @param center un objet avec des propriétés x, y
     * @param radius le rayon
     * @param attr un objet contenant tous les attributs, dont le style.
     * @return l'élément CIRCLE nouveau
     **/
    create_circle(center, radius, attr){
	return create_circle(this.group, center, radius, attr);
    }
    
    /**
     * ajoute un élément TEXT à this.group
     *
     * @param point un objet avec des propriétés x, y
     * @param text le texte
     * @param attr un objet contenant tous les attributs, dont le style.
     * @return l'élément TEXT nouveau
     **/
    create_text(point, text, attr){
	return create_text(this.group, point, text, attr);
    }
    
    /**
     * Ajoute des fonctions de rappel à l'objet, qui lui permettent
     * de réagir au tirer-glisser
     * @param ident identifiant de la famille de fonctions de rappel
     * @param poignee un élément qui sera sensible a clic de début de tirage
     * @param cursor une chaîne de caractère pour le curseur (CSS)
     * @param dragstart la fonction de rappel de début de tirage
     * @param drag      la fonction de rappel pendant le tirage
     * @param dragend   la fonction de rappel à la fin du tirage
     **/
    draggable(ident, poignee, cursor, dragstart, drag, dragend){
	let startsignal = "dragstart."+ident+"."+this.ident;
	let movesignal  = "drag."+ident+"."+this.ident;
	let endsignal   = "dragend."+ident+"."+this.ident;
	draggable(
	    this.group, poignee, this.centre, cursor,
	    startsignal, movesignal, endsignal
	);
	this.group.addEventListener(startsignal, dragstart, false);
	this.group.addEventListener(movesignal,  drag,      false);
	this.group.addEventListener(endsignal,   dragend,   false);
    }

    /**
     * Rend l'objet maniable en translation
     **/
    translatable(){
	let obj = this;
	this.draggable(
	    "translation",
	    this.translate_handle,
	    "move",
	    function(evt){
		obj.bv0 = obj.group.transform.baseVal.consolidate();
	    },
	    function(evt){
		obj.group.transform.baseVal.initialize(obj.bv0);
		obj.translate(evt.data.x, evt.data.y);
	    },
	    function(evt){
		null;
	    }
	);
    }
    
    /**
     * Rend l'objet maniable en rotation
     **/
    rotatable(){
	let obj = this;
	this.draggable(
	    "rotation",
	    this.rotate_handle,
	    "grab",
	    function(evt){
		let r = obj.centre_zero.getBoundingClientRect();
		let s = obj.svg.getBoundingClientRect();
		obj.B = new DOMPoint(
		    r.x + r.width/2 - s.x, r.y + r.height/2 - s.y);
		obj.A = new DOMPoint(evt.data.x, evt.data.y);
		obj.heading = 0;
	    },
	    function(evt){
		/* on a bougé la souris pour faire tourner l'objet */
		let A = obj.A;
		let B = obj.B;
		let C = new DOMPoint(evt.data.x + A.x, evt.data.y + A.y);
		let x = (C.x - B.x) * (B.x - A.x) + (C.y - B.y) * (B.y - A.y);
		let y = (C.y - B.y) * (B.x - A.x) - (C.x - B.x) * (B.y - A.y);
		let angle = -180 + Math.atan2(y,x)/Math.PI*180;
		obj.rotate(angle-obj.heading);
		obj.heading = angle;
	    },
	    function(evt){
		null;
	    }
	);
    }
    
    /**
     * Déplace this.group, d'un vecteur
     * @param dx abscisse
     * @param dy ordonnée
     **/
    translate(dx,dy){
	let bv = this.group.transform.baseVal;
	let svgt = this.svg.createSVGTransform();
	svgt.setRotate(-this.own_heading, this.centre.x, this.centre.y);
	bv.appendItem(svgt);
	bv.consolidate();
	svgt.setTranslate(dx/this.own_scale, dy/this.own_scale);
	bv.appendItem(svgt);
	bv.consolidate();
	svgt.setRotate(this.own_heading, this.centre.x, this.centre.y);
	bv.appendItem(svgt);
	bv.consolidate();
    }

    /**
     * tourne this.group, d'un angle a autour de this.centre_zero
     * @param a un angle en degrés
     **/
    rotate(a){
	let bv = this.group.transform.baseVal;
	let svgt = this.svg.createSVGTransform();
	svgt.setRotate(a, this.centre.x, this.centre.y);
	bv.appendItem(svgt);
	bv.consolidate();
	this.own_heading += a;
    }

    /**
     * zoome this.group, d'un ratio s autour de this.centre_zero
     * @param s le coefficient de l'homotétie
     **/
    scale(s){
	this.own_scale = s * this.own_scale;
	let bv = this.group.transform.baseVal;
	let svgt = this.svg.createSVGTransform();
	svgt.setTranslate(this.centre.x, this.centre.y);
	bv.appendItem(svgt);
	bv.consolidate();
	svgt.setScale(s, s);
	bv.appendItem(svgt);
	bv.consolidate();
	svgt.setTranslate(-this.centre.x, -this.centre.y);
	bv.appendItem(svgt);
	bv.consolidate();
    }
}


/**
 * permet de lire un objet GeomTool depuis un fichier
 **/
class GeomToolFromFile extends GeomTool{
    
    constructor (paper, ident, filename, center, scale){
	super(paper, 0, 0, ident);
	this.paper = paper;
	this.filename = filename;
	this.initial_center = center;
	this.initial_scale = scale;
	this.registerGroup();
    }

    registerGroup(){
	let obj = this;
	if (typeof(ident) !== "undefined"){
	    this.ident=ident;
	} else {
	    this.ident= "id" + Math.floor(Math.random() * 1000000);
	}
	var req = new XMLHttpRequest();
	req.onerror = function(){
	    alert("Le chargement du fichier " + this.filename + " a échoué");
	};
	req.onload = function(){
	    let parser = new DOMParser();
	    let doc = parser.parseFromString(
		this.responseText, "image/svg+xml");
	    let svgRoot = doc.firstElementChild;
	    obj.group = svgRoot.getElementById("layer1");
	    obj.group.setAttribute("id", obj.ident);
	    obj.group.transform.baseVal.initialize(
		obj.svg.createSVGTransformFromMatrix(
		    obj.svg.createSVGMatrix()));
	    obj.svg.append(obj.group);
	    /*
	      les sous-groupes importants ont pour ids
	      "centre", "translation_handle" et "rotation_handle"
	    */
	    for (let i=0; i < obj.group.children.length; i++){
		let child = obj.group.children.item(i);
		if (child.getAttribute("id") == "translation_handle")
		    obj.translate_handle = child;
		if (child.getAttribute("id") == "rotation_handle")
		    obj.rotate_handle = child;
		if (child.getAttribute("id") == "centre"){
		    obj.centre = new DOMPoint(
			child.getAttribute("cx"), child.getAttribute("cy"));
		    obj.centre_zero = create_circle(
			obj.group,
			obj.centre,
			5,
			{
			    fill: "cyan",
			    opacity: 0.001,
			}
		    );
		}
	    }
	    obj.translatable();
	    obj.rotatable();
	    obj.paper.addTool(obj, obj.initial_center, obj.initial_scale);
	};
	req.open('GET', this.filename);
	req.send();
    }
}

class DoubleDecimetre extends GeomTool{

    constructor(paper, cx, cy, ident){
	super(paper, cx, cy, ident);
	this.registerGroup();
    }
    
    /**
     * Initialise les dessins propres au double décimètre
     **/
    dessine() {
	let dd = this.create_rect(new DOMRect(100, 100, 210, 40),{
	    fill: "cyan",
	    "fill-opacity": 0.3,
	    "stroke-opacity": 1,
	    "stroke-width": 0.4,
	    stroke: "black",
	});
	let zone_translation = this.create_rect(
	    new DOMRect(100,115, 110, 25),{
		fill: "red",
		opacity: 0.05,
	    });
	this.translate_handle = zone_translation;
	let zone_rotation = this.create_rect(
	    new DOMRect(210,115, 110, 25),{
		fill: "yellow",
		opacity: 0.05
	});
	this.rotate_handle = zone_rotation;
	let graduation_path = ""
	for (var mm=0; mm<201; mm++){
	    var l = 8;
	    if (mm%10 !=0){
		l = 4.5;	    
	    }
	    graduation_path += `M ${105+mm} ${100} L ${105+mm} ${100+l} `;
	}
	let graduation = this.create_path(graduation_path, {
	    "stroke-width": 0.15,
	    "stroke-opacity": 1,
	    stroke: "black",
	});
	
	for (let cm = 0; cm < 21; cm++){
	    let numero = ""+cm;
	    let t = this.create_text(
		new DOMPoint(105+10*cm, 110),
		numero, {
		    "style": "text-anchor: middle; font-family: Arial; font-size: 5px;",
		}
	    );
	}
    }
}
