337 lines
16 KiB
JavaScript
337 lines
16 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|||
|
|
|
|||
|
|
// Le modèle de dessin du plan (§ 7.3 à § 7.7), sans DOM : ce que PlanDessin
|
|||
|
|
// peint, tiré de l'état des places d'un tour (EtatPlaces, A5) et de la
|
|||
|
|
// géométrie des tables (P2). Le modèle lit les conflits de l'état des
|
|||
|
|
// places, jamais ne les recalcule (§ 7.5) ; il ne rend que des codes, des
|
|||
|
|
// nombres et des données — noms, libellés de titre —, que les composants
|
|||
|
|
// traduisent. Les mesures de texte sont injectées : getComputedTextLength
|
|||
|
|
// au navigateur, une fonction quelconque sous node.
|
|||
|
|
import { constante } from '../../geometrie/constantes.js';
|
|||
|
|
import { regimeCibles } from '../../geometrie/designation.js';
|
|||
|
|
import { geometrieTable } from '../../geometrie/tables.js';
|
|||
|
|
import { decalagesLignes, tronquer } from '../../geometrie/texte.js';
|
|||
|
|
|
|||
|
|
/** Largeur maximale de la barre de l'échelle graphique, en px d'écran. */
|
|||
|
|
export const LARGEUR_ECHELLE_PX = 120;
|
|||
|
|
|
|||
|
|
/** Rayon du disque de la pastille, en cm de dessin : sous la marque de gel
|
|||
|
|
* d'une table de 2, la plus petite. */
|
|||
|
|
export const RAYON_PASTILLE = 15;
|
|||
|
|
/** Corps du chiffre de la pastille, en cm de dessin. */
|
|||
|
|
export const TAILLE_TEXTE_PASTILLE_CM = 18;
|
|||
|
|
|
|||
|
|
/** Écart entre le bord d'une chaise et son anneau, ou sa mise en évidence, en cm. */
|
|||
|
|
export const ECART_ANNEAU = 6;
|
|||
|
|
export const ECART_MISE_EN_EVIDENCE = 12;
|
|||
|
|
|
|||
|
|
// Les cadenas d'une chaise (§ 7.6), en cm, centrés sur l'origine : un corps
|
|||
|
|
// plein et une anse, fermée ou ouverte, remplis d'un seul tenant. Le dessin
|
|||
|
|
// les translate au centre de la chaise. Le centre de la boîte de chacun
|
|||
|
|
// tombe dans son corps.
|
|||
|
|
const CORPS = 'M-8,-6 H8 V9 H-8 Z';
|
|||
|
|
|
|||
|
|
/** Le tracé du cadenas fermé, centré sur l'origine, en cm. */
|
|||
|
|
export const CADENAS_FERME = `${CORPS} M-6,-6 V-10 A6,6 0 0 1 6,-10 V-6 H3.5 V-10 A3.5,3.5 0 0 0 -3.5,-10 V-6 Z`;
|
|||
|
|
/** Le tracé du cadenas ouvert : la branche droite de l'anse ne rejoint pas le corps. */
|
|||
|
|
export const CADENAS_OUVERT = `${CORPS} M-6,-6 V-13 A6,6 0 0 1 6,-13 V-10 H3.5 V-13 A3.5,3.5 0 0 0 -3.5,-13 V-6 Z`;
|
|||
|
|
|
|||
|
|
/** Distance entre le haut du plateau et le centre de la marque de gel, en cm. */
|
|||
|
|
export const RETRAIT_GEL = 13;
|
|||
|
|
|
|||
|
|
/** Le tracé de la marque de gel, un hexagone plein centré sur l'origine, en cm. */
|
|||
|
|
export const TRACE_GEL = 'M0,-8 L7,-4 L7,4 L0,8 L-7,4 L-7,-4 Z';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Les formes d'une table en attributs SVG, en cm : plateau et emprise —
|
|||
|
|
* disques d'une ronde ({cx, cy, r}), carrés aux côtés parallèles aux axes
|
|||
|
|
* d'une carrée ({x, y, width, height}) —, le centre de la pastille, celui du
|
|||
|
|
* plateau, et celui de la marque de gel, dans le plateau, RETRAIT_GEL sous
|
|||
|
|
* son haut.
|
|||
|
|
*
|
|||
|
|
* @param {import('../../geometrie/tables.js').GeometrieTable} geometrie
|
|||
|
|
*/
|
|||
|
|
export function formesTable({ forme, centre, demiTaille, rayonEmprise }) {
|
|||
|
|
const ronde = forme === 'ronde';
|
|||
|
|
const dessiner = (demi) =>
|
|||
|
|
ronde ? { cx: centre.x, cy: centre.y, r: demi } : { x: centre.x - demi, y: centre.y - demi, width: 2 * demi, height: 2 * demi };
|
|||
|
|
return {
|
|||
|
|
ronde,
|
|||
|
|
plateau: dessiner(demiTaille),
|
|||
|
|
emprise: dessiner(rayonEmprise),
|
|||
|
|
pastille: { x: centre.x, y: centre.y },
|
|||
|
|
gel: { x: centre.x, y: centre.y - demiTaille + RETRAIT_GEL },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* La ligne d'une chaise (§ 7.6) : son numéro, l'état de la place — réservée
|
|||
|
|
* pour tous les tours, réservée au tour désigné, libre, ou rien pour un
|
|||
|
|
* occupant sans réservation —, le nom de l'occupant, et le titre de la place.
|
|||
|
|
* La réservation dit l'état de la place ; l'occupant, le nom : l'occupant
|
|||
|
|
* d'une place titrée n'en devient pas titulaire (§ 4.1).
|
|||
|
|
*
|
|||
|
|
* Une réservation dont la titulaire n'occupe pas la chaise — une source
|
|||
|
|
* qui assied une autre personne, ou personne, sur la place réservée — ne
|
|||
|
|
* s'attribue pas à l'occupant : l'état devient plan.ligne.reserveeA, de
|
|||
|
|
* mêmes portée et tour, et titulaire porte le nom de la personne réservée,
|
|||
|
|
* que la ligne énonce dans sa propre partie.
|
|||
|
|
*
|
|||
|
|
* @param {Object} chaise EtatChaise
|
|||
|
|
* @param {(id: number) => string} nomDe le nom affiché d'un participant
|
|||
|
|
* @returns {{numero: number, etat: {cle: string, details: Object}|null, titulaire: string|null,
|
|||
|
|
* nom: string|null, titre: {libelle: string, pourvu: boolean}|null}}
|
|||
|
|
*/
|
|||
|
|
export function ligneListe(chaise, nomDe) {
|
|||
|
|
const { siege, occupant, reservation, titre } = chaise;
|
|||
|
|
let etat = null;
|
|||
|
|
let titulaire = null;
|
|||
|
|
if (reservation !== null && reservation.participant !== occupant) {
|
|||
|
|
etat = { cle: 'plan.ligne.reserveeA', details: { portee: reservation.portee, tour: reservation.tour } };
|
|||
|
|
titulaire = nomDe(reservation.participant);
|
|||
|
|
} else if (reservation !== null) {
|
|||
|
|
etat =
|
|||
|
|
reservation.portee === 'tous'
|
|||
|
|
? { cle: 'plan.ligne.reserveeTous', details: {} }
|
|||
|
|
: { cle: 'plan.ligne.reserveeTour', details: { tour: reservation.tour } };
|
|||
|
|
} else if (occupant === null) {
|
|||
|
|
etat = { cle: 'plan.ligne.libre', details: {} };
|
|||
|
|
}
|
|||
|
|
return {
|
|||
|
|
numero: siege,
|
|||
|
|
etat,
|
|||
|
|
titulaire,
|
|||
|
|
nom: occupant === null ? null : nomDe(occupant),
|
|||
|
|
titre: titre === null ? null : { libelle: titre.libelle, pourvu: titre.pourvu },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Les textes d'une ligne, partie par partie : tete — numéro et état, suivis
|
|||
|
|
* d'une espace quand un nom suit —, teteSansNom — la même sans l'espace,
|
|||
|
|
* quand le nom tombe —, titulaire et suite, nom, avantTitre, titre — le
|
|||
|
|
* libellé seul —, apresTitre ; null pour une partie absente. Leur
|
|||
|
|
* concaténation, dans cet ordre et sans les formes « SansNom », est la
|
|||
|
|
* ligne entière.
|
|||
|
|
*
|
|||
|
|
* Quand la ligne a une titulaire, l'état s'écrit autour de son nom : tete
|
|||
|
|
* porte le numéro et le début de l'état, titulaire le nom de la personne
|
|||
|
|
* réservée, suite la portée — suivie d'une espace quand un nom suit —, et
|
|||
|
|
* suiteSansNom la même sans l'espace ; tete et teteSansNom sont alors
|
|||
|
|
* égales. Sans titulaire, titulaire, suite et suiteSansNom valent null.
|
|||
|
|
*
|
|||
|
|
* @param {ReturnType<typeof ligneListe>} ligne
|
|||
|
|
* @param {(cle: string, details?: Object) => string} t
|
|||
|
|
*/
|
|||
|
|
export function textesLigne({ numero, etat, titulaire = null, nom, titre }, t) {
|
|||
|
|
const tete = (avecNom) => t('plan.ligne.tete', { numero, etat, avecNom });
|
|||
|
|
const espace = nom === null ? '' : ' ';
|
|||
|
|
const reservee = titulaire === null ? null : t(etat.cle, etat.details);
|
|||
|
|
return {
|
|||
|
|
tete: titulaire === null ? tete(nom !== null) : t('plan.ligne.teteReserveeA', { numero }),
|
|||
|
|
teteSansNom: titulaire === null ? tete(false) : t('plan.ligne.teteReserveeA', { numero }),
|
|||
|
|
titulaire,
|
|||
|
|
suite: reservee === null ? null : `${reservee}${espace}`,
|
|||
|
|
suiteSansNom: reservee,
|
|||
|
|
nom,
|
|||
|
|
avantTitre: titre === null ? null : t(titre.pourvu ? 'plan.ligne.avantTitrePourvu' : 'plan.ligne.avantTitreNonPourvu'),
|
|||
|
|
titre: titre === null ? null : titre.libelle,
|
|||
|
|
apresTitre: titre === null || titre.pourvu ? null : t('plan.ligne.apresTitreNonPourvu'),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* La ligne qui tient dans largeur (§ 7.4) : le nom s'abrège, suivi de la
|
|||
|
|
* marque, à la largeur que lui laissent les autres parties ; quand la
|
|||
|
|
* marque n'y tient plus, le nom tombe — la tête, ou la suite, perd son
|
|||
|
|
* espace — et le titre s'abrège à son tour ; quand il ne tient plus, il
|
|||
|
|
* tombe avec ce qui l'entoure, puis le nom de la titulaire s'abrège dans sa
|
|||
|
|
* partie ; enfin une tête encore trop large s'abrège seule. Le numéro, en
|
|||
|
|
* tête, part le dernier.
|
|||
|
|
*
|
|||
|
|
* Les parties de même style se mesurent ensemble, comme le moteur de rendu
|
|||
|
|
* les compose : tête, titulaire, suite, nom et avantTitre droits d'un
|
|||
|
|
* tenant, le titre en italique, apresTitre droit ; un changement de style
|
|||
|
|
* coupe la composition.
|
|||
|
|
*
|
|||
|
|
* @param {ReturnType<typeof textesLigne>} textes
|
|||
|
|
* @param {{mesurer: (s: string) => number, mesurerItalique: (s: string) => number,
|
|||
|
|
* largeur: number, marque: string}} mesures
|
|||
|
|
* @returns {{tete: string, titulaire: string|null, suite: string|null, nom: string|null,
|
|||
|
|
* avantTitre: string|null, titre: string|null, apresTitre: string|null}}
|
|||
|
|
*/
|
|||
|
|
export function ajusterLigne(textes, { mesurer, mesurerItalique, largeur, marque }) {
|
|||
|
|
const { tete, teteSansNom, nom, avantTitre, titre, apresTitre } = textes;
|
|||
|
|
const titulaire = textes.titulaire ?? null;
|
|||
|
|
const suite = textes.suite ?? null;
|
|||
|
|
const suiteSansNom = textes.suiteSansNom ?? null;
|
|||
|
|
// Le début droit de la ligne, jusqu'au nom : tête, titulaire et suite.
|
|||
|
|
const debutAvecNom = `${tete}${titulaire ?? ''}${suite ?? ''}`;
|
|||
|
|
const debutSansNom = `${teteSansNom}${titulaire ?? ''}${suiteSansNom ?? ''}`;
|
|||
|
|
// Largeur de ce qui suit le titre, puis du titre en italique.
|
|||
|
|
const apres = apresTitre === null ? 0 : mesurer(apresTitre);
|
|||
|
|
const italique = (texte) => (texte === null ? 0 : mesurerItalique(texte));
|
|||
|
|
const vide = { nom: null, avantTitre: null, titre: null, apresTitre: null };
|
|||
|
|
|
|||
|
|
if (nom !== null) {
|
|||
|
|
const reste = italique(titre) + apres;
|
|||
|
|
const nomAjuste = tronquer(nom, largeur, (candidat) => mesurer(`${debutAvecNom}${candidat}${avantTitre ?? ''}`) + reste, marque);
|
|||
|
|
if (nomAjuste !== '') return { tete, titulaire, suite, nom: nomAjuste, avantTitre, titre, apresTitre };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (titre !== null) {
|
|||
|
|
const debut = mesurer(`${debutSansNom}${avantTitre}`);
|
|||
|
|
const titreAjuste = tronquer(titre, largeur, (candidat) => debut + italique(candidat) + apres, marque);
|
|||
|
|
if (titreAjuste !== '') return { tete: teteSansNom, titulaire, suite: suiteSansNom, ...vide, avantTitre, titre: titreAjuste, apresTitre };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (titulaire !== null) {
|
|||
|
|
const titulaireAjuste = tronquer(titulaire, largeur, (candidat) => mesurer(`${teteSansNom}${candidat}${suiteSansNom}`), marque);
|
|||
|
|
if (titulaireAjuste !== '') return { tete: teteSansNom, titulaire: titulaireAjuste, suite: suiteSansNom, ...vide };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { tete: tronquer(teteSansNom, largeur, mesurer, marque), titulaire: null, suite: null, ...vide };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// L'ordre des parties d'une ligne, celui de la lecture.
|
|||
|
|
const PARTIES = ['tete', 'titulaire', 'suite', 'nom', 'avantTitre', 'titre', 'apresTitre'];
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Les parties présentes d'une ligne ajustée, dans l'ordre de la lecture :
|
|||
|
|
* chacune son tspan, nommé par data-partie ; le titre seul en italique.
|
|||
|
|
* @param {ReturnType<typeof ajusterLigne>} ligne
|
|||
|
|
* @returns {Array<{partie: string, texte: string}>}
|
|||
|
|
*/
|
|||
|
|
export function partiesLigne(ligne) {
|
|||
|
|
return PARTIES.filter((partie) => (ligne[partie] ?? null) !== null).map((partie) => ({ partie, texte: ligne[partie] }));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Les lignes de base des n lignes d'un bloc dont le haut est à hautY : la
|
|||
|
|
* première une hauteur de ligne sous le haut, chacune une hauteur sous la
|
|||
|
|
* précédente. hauteur est la hauteur d'encre du gabarit (§ 7.4) : deux
|
|||
|
|
* lignes consécutives ne se chevauchent pas.
|
|||
|
|
* @param {number} hautY @param {number} n @param {number} hauteur
|
|||
|
|
* @returns {number[]}
|
|||
|
|
*/
|
|||
|
|
export function ordonneesLignes(hautY, n, hauteur) {
|
|||
|
|
return decalagesLignes(n, hauteur).map((decalage) => hautY + hauteur + decalage);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* La longueur d'une barre d'échelle en mots : sa valeur dans l'unité qui la
|
|||
|
|
* rend lisible — cm sous le mètre, m sous le kilomètre, km au-delà —, le
|
|||
|
|
* nombre de décimales qu'elle demande, et la clé de l'unité.
|
|||
|
|
* @param {number} longueurCm 1, 2 ou 5 × 10ⁿ
|
|||
|
|
* @returns {{valeur: number, decimales: number, cle: string}}
|
|||
|
|
*/
|
|||
|
|
export function libelleLongueur(longueurCm) {
|
|||
|
|
let valeur = longueurCm;
|
|||
|
|
let cle = 'plan.echelle.centimetres';
|
|||
|
|
if (longueurCm >= 100000) {
|
|||
|
|
valeur = longueurCm / 100000;
|
|||
|
|
cle = 'plan.echelle.kilometres';
|
|||
|
|
} else if (longueurCm >= 100) {
|
|||
|
|
valeur = longueurCm / 100;
|
|||
|
|
cle = 'plan.echelle.metres';
|
|||
|
|
}
|
|||
|
|
// Sous l'unité, une mantisse 1, 2 ou 5 × 10⁻ⁿ demande n décimales ; la
|
|||
|
|
// marge absorbe l'arrondi du logarithme d'une puissance de dix.
|
|||
|
|
const decimales = valeur >= 1 ? 0 : Math.ceil(-Math.log10(valeur) - 1e-9);
|
|||
|
|
return { valeur, decimales, cle };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Le bandeau du plan (§ 7.5) : vide tant qu'une table au moins est saine ;
|
|||
|
|
// sinon la phrase, puis collisionPartout quand le diagnostic le prouve, puis
|
|||
|
|
// les planchers — une seule appartenance, ou chaque groupe de collisions,
|
|||
|
|
// ou l'absence de plancher prouvé. Les chiffres sont ceux du moteur, tels
|
|||
|
|
// quels, sans somme ; sans diagnostic, la phrase seule.
|
|||
|
|
function bandeauDe(toutesEnConflit, diagnostic) {
|
|||
|
|
if (!toutesEnConflit) return [];
|
|||
|
|
const entrees = [{ cle: 'plan.bandeau.toutesEnConflit', details: {} }];
|
|||
|
|
if (diagnostic === null) return entrees;
|
|||
|
|
if (diagnostic.collisionPartout) entrees.push({ cle: 'plan.bandeau.collisionPartout', details: {} });
|
|||
|
|
if (diagnostic.uneSeuleAppartenance) {
|
|||
|
|
entrees.push({ cle: 'plan.bandeau.uneSeuleAppartenance', details: {} });
|
|||
|
|
} else if (diagnostic.collisions.length > 0) {
|
|||
|
|
for (const { groupe, effectif, plancherCumulees } of diagnostic.collisions) {
|
|||
|
|
entrees.push({ cle: 'plan.bandeau.plancherGroupe', details: { groupe, effectif, plancherCumulees } });
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
entrees.push({ cle: 'plan.bandeau.sansPlancher', details: {} });
|
|||
|
|
}
|
|||
|
|
return entrees;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Le dessin d'une table de l'état des places au zoom k.
|
|||
|
|
function tableDessin(etatTable, { k, miseEnEvidence, nomDe }) {
|
|||
|
|
const { id, numero, capacite, gelee, surchargee, conflits } = etatTable;
|
|||
|
|
const geometrie = geometrieTable(etatTable, capacite);
|
|||
|
|
const regime = regimeCibles(k, geometrie);
|
|||
|
|
const pastille = conflits === null ? 0 : conflits.nombre;
|
|||
|
|
const chaises =
|
|||
|
|
regime === 'table'
|
|||
|
|
? []
|
|||
|
|
: etatTable.chaises.map((chaise, i) => {
|
|||
|
|
const { x, y, rayon } = geometrie.chaises[i];
|
|||
|
|
return {
|
|||
|
|
siege: chaise.siege,
|
|||
|
|
x,
|
|||
|
|
y,
|
|||
|
|
rayon,
|
|||
|
|
remplissage: chaise.remplissage,
|
|||
|
|
marques: [...chaise.marques],
|
|||
|
|
miseEnEvidence: miseEnEvidence !== null && chaise.occupant === miseEnEvidence,
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
return {
|
|||
|
|
id,
|
|||
|
|
geometrie,
|
|||
|
|
regime,
|
|||
|
|
conflit: pastille > 0,
|
|||
|
|
pastille,
|
|||
|
|
gelee,
|
|||
|
|
surchargee,
|
|||
|
|
nomAccessible: {
|
|||
|
|
cle: 'plan.table.nom',
|
|||
|
|
details: { numero, places: capacite, conflits: conflits === null ? null : conflits.nombre, gelee, surchargee },
|
|||
|
|
},
|
|||
|
|
chaises,
|
|||
|
|
enTete: gelee ? { cle: 'plan.liste.gelee', details: {} } : null,
|
|||
|
|
lignes: etatTable.chaises.map((chaise) => ligneListe(chaise, nomDe)),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Le modèle de dessin d'un tour (§ 7.3 à § 7.7).
|
|||
|
|
*
|
|||
|
|
* Chaque table, dans l'ordre de l'état des places, porte sa géométrie
|
|||
|
|
* (geometrieTable, P2), son régime des cibles au zoom k (regimeCibles) —
|
|||
|
|
* aucune chaise dessinée en régime table —, son conflit et sa pastille, lus
|
|||
|
|
* dans conflits — null : ni conflit, ni pastille —, son nom accessible, et
|
|||
|
|
* une ligne par siège, de 1 à la capacité, précédée de l'en-tête d'une
|
|||
|
|
* table gelée. Quand toutes les tables sont en conflit, aucune n'est
|
|||
|
|
* signalée — signalerConflits faux — et le bandeau énonce le diagnostic ;
|
|||
|
|
* les pastilles gardent leur nombre. Les listes se rendent quand elles
|
|||
|
|
* sont demandées et que k atteint K_LISTES (§ 17, point 6). La largeur du
|
|||
|
|
* bloc de noms n'est pas dans le modèle : c'est LARGEUR_BLOC_NOMS_CM du
|
|||
|
|
* registre, que le dessin lit.
|
|||
|
|
*
|
|||
|
|
* @param {Object} etatPlaces EtatPlaces (A5)
|
|||
|
|
* @param {{k: number, listes: boolean, miseEnEvidence: number|null,
|
|||
|
|
* nomDe: (id: number) => string, diagnostic?: Object|null}} options
|
|||
|
|
* diagnostic : celui que rend diagnostiquer, compléments de M1 compris
|
|||
|
|
* @returns {Object} ModeleDessin
|
|||
|
|
*/
|
|||
|
|
export function modeleDessin(etatPlaces, { k, listes, miseEnEvidence, nomDe, diagnostic = null }) {
|
|||
|
|
const tables = etatPlaces.tables.map((etatTable) => tableDessin(etatTable, { k, miseEnEvidence, nomDe }));
|
|||
|
|
const toutesEnConflit = tables.length > 0 && tables.every(({ conflit }) => conflit);
|
|||
|
|
return {
|
|||
|
|
tables,
|
|||
|
|
signalerConflits: !toutesEnConflit,
|
|||
|
|
bandeau: bandeauDe(toutesEnConflit, diagnostic),
|
|||
|
|
listesRendues: listes && k >= constante('K_LISTES'),
|
|||
|
|
};
|
|||
|
|
}
|