diff --git a/LISEZMOI.rst b/LISEZMOI.rst index da87faf..389e4b6 100644 --- a/LISEZMOI.rst +++ b/LISEZMOI.rst @@ -4,16 +4,18 @@ Parliamentarch Module utilitaire permettant la génération de diagrammes parlementaires en hémicycles. -Base de maths -------------- +Géométrie +--------- L'idée est de placer un certain nombre de sièges en forme d'hémicycle. La forme recherchée peut être définie comme suit : - Prenez deux cercles concentriques tels que le rayon du cercle extérieur soit le double du rayon du cercle intérieur. + - Prenez l'aire séparant les deux cercles, appelée un anneau. -- Divisez cet anneau en deux par un diamètre du cercle extérieur. + +- Divisez cet anneau en deux le long d'un diamètre du cercle extérieur. Le résultat est un hémicycle. Maintenant, pour placer les sièges tels qu'ils suivent la forme de l'hémicycle : @@ -21,22 +23,34 @@ suivent la forme de l'hémicycle : - L'hémicycle rentre dans un rectangle de proportions 2:1, pris en orientation paysage avec le diamètre de coupe en bas (les coins supérieur gauche et supérieur droit du rectangle sont vides). + - Les sièges sont placés en rangées, tel que : + - Les rangées sont des arcs de cercle, concentriques avec les arcs de cercle formant l'hémicycle. + - La différence entre les rayons de deux rangées consécutives est une constante appelée "épaisseur de rangée". + - Les sièges sont des cercles (ou des disques) de rayon égal. Ce rayon divisé par la moitié de l'épaisseur de rangée donne le "ratio de rayon de siège". + - Le centre d'un siège est placé sur l'arc de la rangée dont il fait partie. + - Dans une rangée donnée, la distance entre deux sièges voisins est constante. + - La rangée la plus intérieure est l'arc de cercle le plus petit ayant servi à définir l'hémicycle. + - Le rayon de la rangée la plus extérieure est égal au rayon du cercle extérieur moins la moitié de l'épaisseur de rangée, de manière à ce qu'aucun siège ne puisse mordre l'arc de cercle extérieur. - - Les sièges les plus bas de chaque rangée, qui sont le premier et le dernier - siège de la rangée, sont tangents au côté inférieur du rectangle. + + - La distance verticale entre le centre des sièges les plus bas de chaque + rangée, qui sont le premier et le dernier siège de la rangée, est égale à la + moitié de l'épaisseur de rangée, de manière à ce qu'aucun siège ne puisse + mordre l'arc de cercle intérieur. + - Quand une rangée ne contient qu'un siège, la règle précédente ne s'applique pas et le siège est placé au milieu horizontal du diagramme. @@ -62,10 +76,13 @@ changer la forme de l'hémicycle. l'angle est suffisament aigü pour empêcher une rangée de contenir un seul siège n'est pas supporté, peut donner des résultats incorrects, et pourra causer des erreurs dans des versions futures. + - Le nombre de rangées peut être augmenté par rapport au minimum nécessaire pour contenir le nombre de sièges donné. + - Le ratio de rayon de siège peut être ajusté entre 0 et 1, avec les sièges touchant leurs voisins latéraux avec une valeur de 1. + - Tant que le nombre de sièges n'est pas le nombre maximum que le nombre de rangées peut contenir, différentes stratégies peuvent être suivies pour les répartir entre les rangées. @@ -80,7 +97,7 @@ Ces éléments se trouvent dans le module ``parliamentarch``. Cette classe est définie et expliquée dans le sous-module SVG ci-dessous, mais il est exposé au sein du module principal. -``write_svg_from_attribution(file, attrib, **kwargs)`` +``write_svg_from_attribution(file, attrib, *, seat_radius_factor: float = 1., **kwargs)`` Cette fonction écrit un fichier SVG représentant un hémicycle. Les paramètres sont les suivants : @@ -89,11 +106,17 @@ sont les suivants : vers le fichier sur lequel écrire. Si un chemin est fourni, le fichier sera écrit sous l'encodage UTF-8, et il sera créé si il n'existe pas et sinon réécrit. + - ``attrib: dict[SeatData, int]`` : un dictionnaire d'objets SeatData s'appliquant à un ensemble de sièges présents dans l'hémicycle, vers le nombre de sièges auxquels l'objet s'applique. Typiquement, chaque objet correspond à un parti ou un groupe. L'ordre des clés a un sens, et les éléments seront disposés de gauche à droite dans l'hémicycle. + +- ``seat_radius_factor`` : le ratio de rayon de siège, égal au rayon du siège + divisé par l'épaisseur de rangée. Par défaut, à 1, les sièges peuvent toucher + leurs voisins. + - ``**kwargs`` : tous les paramètres optionnels acceptés par ``parliamentarch.geometry.get_seats_centers`` ou par ``parliamentarch.svg.write_svg`` peuvent être passés à cette fonction. @@ -124,6 +147,14 @@ de sièges dans un diagramme s'étendant sur un angle donné. liste du nombre maximum de sièges que chaque rangée peut contenir, de l'intérieur vers l'extérieur. La liste est croissante et sa longueur est égale au nombre de rangées. +``get_row_thickness(nrows: int) -> float`` + +À partir d'un nombre de rangées donné, renvoie l'épaisseur de rangée, c'est à +dire la différence entre les rayons de deux rangées consécutives. Cette fonction +peut aider à convertir un ratio de rayon de siège (tel que passé à +``write_svg_from_attribution``) en un rayon de siège tel que pris par +``write_svg``. + ``FillingStrategy`` Énumération des différentes stratégies de répartition des sièges entre les @@ -132,6 +163,7 @@ rangées. Les stratégies implémentées sont les suivantes : - ``FillingStrategy.DEFAULT`` : Répartit les sièges de manière proportionnelle au nombre maximum de sièges que chaque rangée peut contenir. Le résultat rend la distance latérale entre des sièges voisins similaire entre les rangées. + - ``FillingStrategy.EMPTY_INNER`` : Sélectionne le nombre minimal de rangées extérieures nécessaires pour contenir le nombre de sièges donné, puis distribue les sièges de manière proportionnelle entre ces rangées. En fonction @@ -139,39 +171,30 @@ rangées. Les stratégies implémentées sont les suivantes : vides, soit le résultat sera identique à la stratégie ``DEFAULT``. Sans compter les rangées vides, la distance entre des sièges voisins est à la fois minimale, et proche d'une rangée à l'autre. + - ``FillingStrategy.OUTER_PRIORITY`` : Remplit les rangées à leur capacité maximale, de l'extérieur vers l'intérieur. Le résultat est qu'avec un nombre donné de rangées, ajouter un siège ne modifie qu'une seule rangée. -``get_seats_centers(nseats: int, *, min_nrows: int = 0, span_angle: float = 180., seat_radius_factor: float = 1., filling_strategy: FillingStrategy = FillingStrategy.DEFAULT) -> List[Tuple[float, float]]`` +``get_seats_centers(nseats: int, *, min_nrows: int = 0, span_angle: float = 180., filling_strategy: FillingStrategy = FillingStrategy.DEFAULT) -> List[Tuple[float, float]]`` La fonction principale. En-dehors des paramètres évidents ou équivalents aux fonctions précédentes : - ``min_nrows`` : le nombre minimum de rangées à utiliser. Uniquement pris en compte si la valeur est supérieure au nombre de rangées nécessaires pour contenir le nombre de sièges donné. -- ``seat_radius_factor`` : le ratio de rayon de siège, égal au rayon du siège - divisé par l'épaisseur de rangée. Par défaut, à 1, les sièges peuvent toucher - leurs voisins. -La fonction renvoie un objet similaire à un dictionnaire représentant l'ensemble -des sièges. Les clés sont ``(x, y)``, les coordonnées cartésiennes du centre du -siège. Les coordonnées partent du coin inférieur gauche du rectangle, avec l'axe -x vers la droite et l'axe y vers le haut. Le rayon de l'arc extérieur (égal à la -hauteur et à la moitié de la largeur du rectangle) est 1, donc x va de 0 à 2 et -y de 0 à 1. +La fonction renvoie un dictionnaire représentant l'ensemble des sièges. Les clés +sont ``(x, y)``, les coordonnées cartésiennes du centre du siège. Les +coordonnées partent du coin inférieur gauche du rectangle, avec l'axe x vers la +droite et l'axe y vers le haut. Le rayon de l'arc extérieur (égal à la hauteur +et à la moitié de la largeur du rectangle) est 1, donc x va de 0 à 2 et y de 0 +à 1. La valeur pour chaque clé est l'angle, en radian, depuis le point le plus extérieur et à droite de l'arc d'anneau, vers le centre des arcs, jusqu'au centre du siège. -De plus, la valeur de retour contient les attributs suivants : - -- ``di.seat_actual_radius`` : le rayon des sièges, dans la même unité que les - coordonnées. -- ``di.nrows`` : comme passé à la fonction. -- ``di.seat_radius_factor`` : comme passé à la fonction. - Appeler ``sorted(di, key=di.get, reverse=True)`` renvoie la liste des sièges triée de gauche à droite. @@ -186,13 +209,16 @@ Une classe informant la représentation d'un siège ou d'un groupe de sièges. - ``data: str`` : métadonnées à propos du groupe de sièges, qui finira dans le fichier SVG. Typiquement le nom du parti ou de l'élu. + - ``color: Color`` : la couleur de remplissage du cercle représentant le siège. Accepte divers formats de données : une string "#RGB", "#RRGGBB", "#RGBA" ou "#RRGGBBAA", un ``tuple[int, int, int]`` RGB, ou un ``tuple[int, int, int, int]`` RGBA avec des entiers entre 0 et 255. Les noms de couleurs CSS sont aussi acceptés. + - ``border_size: float`` : la taille de la bordure du cercle représentant le siège. (à documenter avec plus de détails) + - ``border_color: Color`` : la couleur de la bordure. ``write_svg(file, seat_centers, seat_actual_radius, *, canvas_size=175, margins=5., write_number_of_seats=True, font_size_factor=...)`` @@ -204,20 +230,27 @@ sont les suivants : vers le fichier sur lequel écrire. Si un chemin est fourni, le fichier sera écrit sous l'encodage UTF-8, et il sera créé si il n'existe pas et sinon réécrit. + - ``seat_centers: dict[tuple[float, float], SeatData]`` : un dictionnaire des coordonnées (x, y) des centres des sièges vers des objets SeatData. -- ``seat_actual_radius: float`` : le rayon des sièges, tel que renvoyé par - ``get_seats_centers``. + +- ``seat_actual_radius: float`` : le rayon des sièges, dans la même unité que + les coordonnées renvoyées par ``get_seat_centers``, c'est à dire une fraction + de ``canvas_size``. + - ``canvas_size: float`` : la hauteur du rectangle 2:1 dans lequel l'hémicycle est inscrit. + - ``margins: float|tuple[float, float]|tuple[float, float, float, float]`` : les marges autour de ce rectangle. Si quatre valeurs sont données, elles sont la marge gauche, supérieure, droite et inférieure, dans cet ordre. Si deux valeurs sont données, elles sont la marge horizontale et la marge verticale, dans cet ordre. Si une seule valeur est donnée, elle est utilisée pour les quatre marges. + - ``write_number_of_seats: bool`` : si le nombre total de sièges est inscrit en bas au milieu du diagramme - au niveau du perchoir. + - ``font_size_factor: float`` : un facteur à modifier pour changer la taille de police du nombre de sièges. La valeur par défaut est proche de 0.2. Garder cette valeur constante gardera la taille de police à la même échelle quand @@ -247,6 +280,7 @@ Une fonction qui aide le passage de ``parliamentarch.get_seats_centers`` à - ``group_seats: dict[SeatData, int]`` : un dictionnaire de l'objet SeatData d'un groupe de sièges vers le nombre de sièges dans ce groupe. L'ordre des clés compte. + - ``seats: Iterable[S]`` : un itérable de sièges dans n'importe quel format, typiquement des tuples (x, y). La taille de l'itérable doit être égale à la somme des valeurs de ``group_seats``. L'ordre des données compte. diff --git a/README.rst b/README.rst index 58b1474..f0b4396 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,9 @@ The idea is to lay a certain number of seats in the form of a hemicycle. The shape we're after can be mathematically described as follows: - Take two concentric circles where the outer one's radius is twice the inner's + - Consider the area between the two, which is called an annulus. + - Cut the annulus in half, following a diameter of the larger circle. The result is the hemicycle. Now, to place the seats so that they make the form @@ -19,9 +21,10 @@ of the hemicycle: - The hemicycle fits in a 2:1 rectangle, which we will consider in a "landscape" orientation, and with the cut diameter being on the bottom (the top left and top right corners of the rectangle are empty). + - The seats will placed in rows, such that: - - The rows are semicircular arcs concentric to the inner and outer arcs. + - The rows are semicircular arcs, concentric to the inner and outer arcs. - The difference between the radii of two consecutive rows is a constant called the "row thickness" (radii is the plural of radius). @@ -30,14 +33,18 @@ of the hemicycle: of the row thickness makes the "seat radius factor". - The center of a seat is on the arc of that seat's row. - - In a given row, the distance between two neighboring seats is a constant. - - The innermost row's arc is the inner arc. + + - In any given row, the distance between two neighboring seats is a constant. + + - The innermost row's arc is the inner arc of the annulus. + - The radius of the outermost row's arc is equal to the radius of the outer arc minus half of the row thickness, such that no seat may overlap the outer arc. - - The bottom-most seats of each row, which means the first and last seat of - each row, are tangent to the bottom of the rectangle. + - The vertical distance between the center of the bottom-most seats of each + row, which are the first and last seat of each row, is equal to half of the + row thickness, such that no seat may overlap the inner arc. - When only one seat is placed in a row, the previous rule does not apply and the seat is placed at the horizontal center of the diagram. @@ -61,10 +68,13 @@ layout of the hemicycle. Among them: values are not supported). However, values so low as to prevent some row from containing even one seat are not supported, will yield incorrect results, and may raise errors in future versions. + - The number of rows can be set higher than the minimum required to hold the provided number of seats. + - The seat radius factor can be changed between 0 and 1, with the seats touching their lateral neighbors when the factor is 1. + - As long as the number of seats is not the maximum number the number of rows can hold, different strategies can be chosen to distribute the seats. @@ -78,7 +88,7 @@ These are found in the ``parliamentarch`` module. This class is defined and explained in the SVG submodule below, but it is exposed as part of the main module. -``write_svg_from_attribution(file, attrib, **kwargs)`` +``write_svg_from_attribution(file, attrib, *, seat_radius_factor: float = 1., **kwargs)`` This function writes an SVG file representing a hemicycle. The parameters are as follows: @@ -87,11 +97,16 @@ follows: to the file to write on. If a path is provided, the file will be written to in UTF-8 encoding, and it will be created if it doesn't exist or overwritten if it does. + - ``attrib: dict[SeatData, int]``: a mapping from a SeatData object applying to a number of seats in the resulting hemicycle, to the number of seats each object applies to. Typically, each SeatData object corresponds to a group or party. The ordering of the keys matter, and the elements will be arranged from left to right in the hemicycle. + +- ``seat_radius_factor: float``: the ratio of the seats radius over the row + thickness. Defaults to 1, which makes seats touch their neighbors. + - ``**kwargs``: all optional keyword parameters taken by ``parliamentarch.geometry.get_seats_centers`` or by ``parliamentarch.svg.write_svg`` can be passed to this function. @@ -122,6 +137,13 @@ From a given number of rows (and span angle), returns a list of each row's maximum seat capacity, starting from inner to outer. The list is increasing and its length is the number of rows. +``get_row_thickness(nrows: int) -> float`` + +From a given number of rows, returns the thickness of the row, i.e the +difference between the radii of successive rows. This can help converting a seat +radius factor (as passed to ``write_svg_from_attribution``) to a seat radius as +taken by ``write_svg``. + ``FillingStrategy`` This is an enumeration of the different implemented strategies to fill the seats @@ -130,6 +152,7 @@ among the rows. The strategies are: - ``FillingStrategy.DEFAULT``: The seats are distributed proportionally to the maximum number of seats each row can hold. The result is that the lateral distance between neighboring seats is close between the rows. + - ``FillingStrategy.EMPTY_INNER``: This selects as few outermost rows as necessary to hold the given seats, then distributes the seats proportionally among them. Depending on the number of seats and of rows, this either leaves @@ -137,11 +160,12 @@ among the rows. The strategies are: equivalent to the legacy "dense rows" option, in that not counting the potential empty rows, the distance between neighboring seats is the smallest possible, and is close between the rows. + - ``FillingStrategy.OUTER_PRIORITY``: This fills the rows to their maximum capacity, starting with the outermost rows going in. The result is that given a number of rows, adding one seat makes a change in only one row. -``get_seats_centers(nseats: int, *, min_nrows: int = 0, span_angle: float = 180., seat_radius_factor: float = 1., filling_strategy: FillingStrategy = FillingStrategy.DEFAULT) -> list[tuple[float, float]]`` +``get_seats_centers(nseats: int, *, min_nrows: int = 0, span_angle: float = 180., filling_strategy: FillingStrategy = FillingStrategy.DEFAULT) -> list[tuple[float, float]]`` This is the main function. Other than self-explanatory parameters similar to the functions above: @@ -149,29 +173,18 @@ the functions above: - ``min_nrows``: The minimum number of rows to use. Only taken into account if the required number of rows to hold the given number of seats is less than that. Defaults to 0, which means using the minimum number of rows possible. -- ``seat_radius_factor``: The ratio of the seats radius over the row thickness. - Defaults to 1, which makes seats touch their neighbors. -The function returns a dict-like object representing the ensemble of seats. The -keys are ``(x, y)``, the cartesian coordinates of the center of the seat. The -coordinates start from the bottom-left corner of the rectangle, with the x axis -pointing right and the y axis pointing up. The radius of the outermost circle -(equal to the height and half the width of the rectangle) is 1, so x goes from -0 to 2 and y goes from 0 to 1. +The function returns a dict representing the ensemble of seats. The keys are +``(x, y)``, the cartesian coordinates of the center of the seat. The coordinates +start from the bottom-left corner of the rectangle, with the x axis pointing +right and the y axis pointing up. The radius of the outermost circle (equal to +the height and half the width of the rectangle) is 1, so x goes from 0 to 2 and +y goes from 0 to 1. The value of each entry is the angle, in radians, calculated from the right-outermost point of the annulus arc, to the center of the arcs, to the center of the seat. -In addition, the return value has the following attributes: - -- ``row_thickness``: the thickness of the rows, in the same unit as the - coordinates. -- ``seat_actual_radius``: the radius of the seats, in the same unit as the - coordinates. -- ``nrows``: as passed to the function. -- ``seat_radius_factor``: as passed to the function. - Calling ``sorted(di, key=di.get, reverse=True)`` will return a list of the seats arranged from left to right. @@ -186,12 +199,15 @@ A class representing how to display a given seat or set of seats. - ``data: str``: metadata about the group of seats, which will end up in the SVG file. Typically the name of the party or of the member. + - ``color: Color``: the color with which to fill the seat circles. This may take any number of formats: a "#RGB", "#RRGGBB", "#RGBA" or "#RRGGBBAA" string, a RBG ``tuple[int, int, int]``, or a RGBA ``tuple[int, int, int, int]`` with ints between 0 and 255. CSS color names are also supported. + - ``border_size: float``: the size of the border around the seat circle. (to be documented at greater length) + - ``border_color: Color``: the color of the border. ``write_svg(file, seat_centers, seat_actual_radius, *, canvas_size=175, margins=5., write_number_of_seats=True, font_size_factor=...)`` @@ -203,18 +219,26 @@ follows: to the file to write on. If a path is provided, the file will be written to in UTF-8 encoding, and it will be created if it doesn't exist or overwritten if it does. + - ``seat_centers: dict[tuple[float, float], SeatData]``: a mapping from the (x, y) coordinates of each seat's center to a SeatData object. -- ``seat_actual_radius: float``: as output by ``get_seats_centers``. + +- ``seat_actual_radius: float``: the seat radius, in the same unit as the + coordinates returned by ``get_seat_centers``, i.e as a fraction of + ``canvas_size``. + - ``canvas_size: float``: the height of the 2:1 rectangle in which the hemicycle will be drawn. + - ``margins: float|tuple[float, float]|tuple[float, float, float, float]``: the margins around that rectangle. If four values are given, they are the left, top, right, and bottom margins, in that order. If two values are given, they are the horizontal and vertical margins, in that order. If one value is given, it is used for all four margins. + - ``write_number_of_seats: bool``: whether to write the total number of seats at the bottom center of the diagram - in the well of the House. + - ``font_size_factor: float``: a factor you should tweak to change the font size of the number of seats. The default value is around 0.2. Keeping this constant will keep the font size in scale when changing the canvas size. @@ -241,6 +265,7 @@ expects it: - ``group_seats: dict[SeatData, int]``: a mapping from the SeatData of a group of seats to the number of seats in that group. Key ordering matters. + - ``seats: Iterable[S]``: an iterable of seats in whatever format, but intended to be (x, y) tuples. Its length must be the sum of the values of ``group_seats``. Its ordering matters. @@ -254,11 +279,19 @@ Todos and future features ------------------------- - Have the main functions support a sequence of SeatData objects using ``dict.fromkeys(seq, 1)`` + - Allow SeatData to take some element properties (like href), and if so use instead of + - Allow SeatData to contain more creative SVG content like gradients + - Maybe give it a .wrap method that wraps the circles in a g or a, and make it subclassable ? + - Maybe just give a style method ? + - Add tests + - Add the option to force all rows to contain an even number of seats + - Add a simpler way to input parameters in CLI + - Maybe by allowing the use of the standard input to pass JSON content ? diff --git a/src/parliamentarch/__init__.py b/src/parliamentarch/__init__.py index 7abfe80..483f40d 100644 --- a/src/parliamentarch/__init__.py +++ b/src/parliamentarch/__init__.py @@ -1,6 +1,6 @@ from inspect import signature -from .geometry import get_seats_centers +from .geometry import get_nrows_from_nseats, get_row_thickness, get_seats_centers from .svg import SeatData, dispatch_seats, get_grouped_svg from ._util import filter_kwargs, write_from_get @@ -9,7 +9,7 @@ _GET_SEATS_CENTERS_PARAM_NAMES = {k: p for k, p in signature(get_seats_centers).parameters.items() if p.kind==p.KEYWORD_ONLY} _WRITE_GROUPED_SVG_PARAM_NAMES = {k: p for k, p in signature(get_grouped_svg).parameters.items() if p.kind==p.KEYWORD_ONLY} -def get_svg_from_attribution(attrib: dict[SeatData, int], **kwargs) -> str: +def get_svg_from_attribution(attrib: dict[SeatData, int], *, seat_radius_factor: float = 1., **kwargs) -> str: nseats = sum(attrib.values()) get_seats_centers_kwargs, write_grouped_svg_kwargs, kwargs = filter_kwargs(_GET_SEATS_CENTERS_PARAM_NAMES, _WRITE_GROUPED_SVG_PARAM_NAMES, **kwargs) @@ -18,15 +18,15 @@ def get_svg_from_attribution(attrib: dict[SeatData, int], **kwargs) -> str: results = get_seats_centers(nseats, **get_seats_centers_kwargs) seat_centers_by_group = dispatch_seats(attrib, sorted(results, key=results.__getitem__, reverse=True)) - return get_grouped_svg(seat_centers_by_group, results.seat_actual_radius, **write_grouped_svg_kwargs) + seat_actual_radius = seat_radius_factor * get_row_thickness(get_nrows_from_nseats(nseats)) + return get_grouped_svg(seat_centers_by_group, seat_actual_radius, **write_grouped_svg_kwargs) _sig = signature(get_svg_from_attribution) -_attrib_param = _sig.parameters["attrib"] get_svg_from_attribution.__signature__ = _sig.replace(parameters=( - _attrib_param, + *(p for p in _sig.parameters.values() if p.kind!=p.VAR_KEYWORD), *_GET_SEATS_CENTERS_PARAM_NAMES.values(), *_WRITE_GROUPED_SVG_PARAM_NAMES.values() )) -del _sig, _attrib_param +del _sig write_svg_from_attribution = write_from_get(get_svg_from_attribution) diff --git a/src/parliamentarch/geometry.py b/src/parliamentarch/geometry.py index ac3664a..bb204cc 100644 --- a/src/parliamentarch/geometry.py +++ b/src/parliamentarch/geometry.py @@ -2,14 +2,12 @@ import functools import math -from ._util import UnPicklable - -__all__ = ("get_rows_from_nrows", "get_nrows_from_nseats", "FillingStrategy", "get_seats_centers") +__all__ = ("get_row_thickness", "get_rows_from_nrows", "get_nrows_from_nseats", "FillingStrategy", "get_seats_centers") # default angle, in degrees, coming from the rightmost seats through the center to the leftmost seats _DEFAULT_SPAN_ANGLE = 180 -def _get_row_thickness(nrows: int) -> float: +def get_row_thickness(nrows: int) -> float: """ Returns the thickness of a row in the same unit as the coordinates. """ @@ -26,7 +24,7 @@ def get_rows_from_nrows(nrows: int, span_angle: float = _DEFAULT_SPAN_ANGLE) -> # thickness of a row (as an annulus) compared to the outer diameter of the hemicycle # this is equal to the diameter of a single seat - rad = _get_row_thickness(nrows) + rad = get_row_thickness(nrows) # if you divide the half-disk of the hemicycle # into one half-disk of half the radius # and one half-annulus outside it, @@ -42,9 +40,9 @@ def get_rows_from_nrows(nrows: int, span_angle: float = _DEFAULT_SPAN_ANGLE) -> for r in range(nrows): # row radius : the radius of the circle crossing the center of each seat in the row - R = .5 + 2*r*rad + row_arc_radius = .5 + 2*r*rad - rv.append(int(radian_span_angle*R/(2*rad))) + rv.append(int(radian_span_angle*row_arc_radius/(2*rad))) return rv @@ -88,27 +86,11 @@ def __new__(cls, value, doc): Fills up the rows as much as possible, starting with the outermost ones. """ -class _SeatsCenterContainer(dict[tuple[float, float], float], UnPicklable): - """ - Stores the coordinates of each seat as keys, the counterclockwise angle as values, - and other data as attributes. - """ - seat_radius_factor: float - nrows: int - - @property - def row_thickness(self): - return _get_row_thickness(self.nrows) - @property - def seat_actual_radius(self): - return self.seat_radius_factor * _get_row_thickness(self.nrows) - def get_seats_centers(nseats: int, *, min_nrows: int = 0, filling_strategy: FillingStrategy = FillingStrategy.DEFAULT, - seat_radius_factor: float = 1, span_angle: float = _DEFAULT_SPAN_ANGLE, - ) -> _SeatsCenterContainer: + ) -> dict[tuple[float, float], float]: """ Returns a list of nseats seat centers as (angle, x, y) tuples. The canvas is assumed to be of 2 in width and 1 in height, with the y axis pointing up. @@ -133,8 +115,7 @@ def get_seats_centers(nseats: int, *, """ nrows = max(min_nrows, _cached_get_nrows_from_nseats(nseats, span_angle)) # thickness of a row in the same unit as the coordinates - row_thicc = _get_row_thickness(nrows) - seat_radius = row_thicc * seat_radius_factor + row_thicc = get_row_thickness(nrows) span_angle_margin = (1 - span_angle/180)*math.pi /2 maxed_rows = _cached_get_rows_from_nrows(nrows, span_angle) @@ -173,7 +154,7 @@ def get_seats_centers(nseats: int, *, case _: raise ValueError(f"Unrecognized strategy : {filling_strategy}") - positions = _SeatsCenterContainer() + positions = {} for r in range(starting_row, nrows): if r == nrows-1: # if it's the last, outermost row # fit all the remaining seats @@ -190,10 +171,10 @@ def get_seats_centers(nseats: int, *, # nseats_this_row = round((nseats-len(positions)) * maxed_rows[r]/sum(maxed_rows[r:])) # row radius : the radius of the circle crossing the center of each seat in the row - R = .5 + 2*r*row_thicc + row_arc_radius = .5 + 2*r*row_thicc # the angle necessary in this row to put the first (and last) seats fully in the canvas - angle_margin = math.asin(seat_radius/R) + angle_margin = math.asin(row_thicc/row_arc_radius) # add the margin to make up the side angle angle_margin += span_angle_margin # alternatively, allow the centers of the seats by the side to reach the angle's boundary @@ -205,13 +186,11 @@ def get_seats_centers(nseats: int, *, # keeping in mind that the same elevation on start and end limits that remaining place to less than 2pi if nseats_this_row == 1: - positions[1., R] = math.pi/2 + positions[1., row_arc_radius] = math.pi/2 else: for s in range(nseats_this_row): angle = angle_margin + s*angle_increment # an oriented angle, so it goes trig positive (counterclockwise) - positions[R*math.cos(angle)+1, R*math.sin(angle)] = angle + positions[row_arc_radius*math.cos(angle)+1, row_arc_radius*math.sin(angle)] = angle - positions.seat_radius_factor = seat_radius_factor - positions.nrows = nrows return positions