"""Helper functions for legend handlers used for plotting."""
# pylint: disable=too-few-public-methods
import numpy as np
from simtools.visualization.matplotlib_backend import lazy_module
mcolors = lazy_module("matplotlib.colors")
mpatches = lazy_module("matplotlib.patches")
# Properties of telescope types for visualization. Radii are relative to REFERENCE_RADIUS.
TELESCOPE_CONFIG = {
"LST": {"color": "darkorange", "radius": 12.5, "shape": "circle", "filled": False},
"MST": {"color": "dodgerblue", "radius": 9.15, "shape": "circle", "filled": False},
"SCT": {"color": "black", "radius": 7.15, "shape": "square", "filled": False},
"SST": {"color": "darkgreen", "radius": 3.0, "shape": "circle", "filled": False},
"HESS": {"color": "grey", "radius": 6.0, "shape": "hexagon", "filled": True},
"MAGIC": {"color": "grey", "radius": 8.5, "shape": "hexagon", "filled": True},
"VERITAS": {"color": "grey", "radius": 6.0, "shape": "hexagon", "filled": True},
"CEI": {"color": "purple", "radius": 2.0, "shape": "hexagon", "filled": True},
"RLD": {"color": "brown", "radius": 2.0, "shape": "hexagon", "filled": True},
"STP": {"color": "olive", "radius": 2.0, "shape": "hexagon", "filled": True},
"MSP": {"color": "teal", "radius": 2.0, "shape": "hexagon", "filled": True},
"ILL": {"color": "red", "radius": 2.0, "shape": "hexagon", "filled": False},
"WST": {"color": "maroon", "radius": 2.0, "shape": "hexagon", "filled": True},
"ASC": {"color": "cyan", "radius": 2.0, "shape": "hexagon", "filled": True},
"DUS": {"color": "magenta", "radius": 2.0, "shape": "hexagon", "filled": True},
}
REFERENCE_RADIUS = 12.5
[docs]
def get_telescope_config(telescope_type):
"""
Return the configuration for a given telescope type.
Try both site-dependent and site-independent configurations (e.g. "MSTS" and "MST").
Parameters
----------
telescope_type : str, None
The type of the telescope (e.g., "LSTN", "MSTS").
Returns
-------
dict
The configuration dictionary for the telescope type.
"""
if telescope_type is None:
return {"color": "blue", "radius": 2.0, "shape": "hexagon", "filled": True}
config = TELESCOPE_CONFIG.get(telescope_type)
if not config and len(telescope_type) >= 3:
config = TELESCOPE_CONFIG.get(telescope_type[:3])
return config.copy() if config else None
[docs]
def calculate_center(handlebox, width_factor=3, height_factor=3):
"""Calculate the center of the handlebox based on given factors."""
x0 = handlebox.xdescent + handlebox.width / width_factor
y0 = handlebox.ydescent + handlebox.height / height_factor
return x0, y0
# Object classes for legend mapping
[docs]
class PixelObject:
"""Pixel Object."""
[docs]
class EdgePixelObject:
"""Edge-Pixel Object."""
[docs]
class OffPixelObject:
"""Off-Pixel Object."""
class _BaseHexPixelHandler:
"""Base class for hexagonal pixel handlers; subclasses set ``colors`` (face, edge)."""
colors = ("black", "black")
@staticmethod
def _create_hex_patch(handlebox, facecolor, edgecolor):
"""Create a hexagonal patch with specified colors."""
x0, y0 = calculate_center(handlebox)
patch = mpatches.RegularPolygon(
(x0, y0),
numVertices=6,
radius=0.7 * handlebox.height,
orientation=np.deg2rad(30),
facecolor=facecolor,
edgecolor=edgecolor,
transform=handlebox.get_transform(),
)
handlebox.add_artist(patch)
return patch
def legend_artist(self, _, __, ___, handlebox):
"""Legend artist."""
return self._create_hex_patch(handlebox, *self.colors)
[docs]
class HexPixelHandler(_BaseHexPixelHandler):
"""Legend handler class to plot a hexagonal "on" pixel."""
colors = ((1, 1, 1, 0), (0, 0, 0, 1))
[docs]
class HexEdgePixelHandler(_BaseHexPixelHandler):
"""Legend handler class to plot a hexagonal "edge" pixel."""
colors = ((0.6470588235294118, 0.16470588235294117, 0.16470588235294117, 0.5), (0, 0, 0, 1))
[docs]
class HexOffPixelHandler(_BaseHexPixelHandler):
"""Legend handler class to plot a hexagonal "off" pixel."""
colors = ("black", "black")
class _BaseSquarePixelHandler:
"""Base class for square pixel handlers; subclasses set ``colors`` (face, edge)."""
colors = ("black", "black")
@staticmethod
def _create_square_patch(handlebox, facecolor, edgecolor):
"""Create a square patch with specified colors."""
x0, y0 = handlebox.xdescent, handlebox.ydescent
width = height = handlebox.height
patch = mpatches.Rectangle(
[x0, y0],
width,
height,
facecolor=facecolor,
edgecolor=edgecolor,
transform=handlebox.get_transform(),
)
handlebox.add_artist(patch)
return patch
def legend_artist(self, _, __, ___, handlebox):
"""Legend artist."""
return self._create_square_patch(handlebox, *self.colors)
[docs]
class SquarePixelHandler(_BaseSquarePixelHandler):
"""Legend handler class to plot a square "on" pixel."""
colors = ((1, 1, 1, 0), (0, 0, 0, 1))
[docs]
class SquareEdgePixelHandler(_BaseSquarePixelHandler):
"""Legend handler class to plot a square "edge" pixel."""
colors = ((0.6470588235294118, 0.16470588235294117, 0.16470588235294117, 0.5), (0, 0, 0, 1))
[docs]
class SquareOffPixelHandler(_BaseSquarePixelHandler):
"""Legend handler class to plot a square "off" pixel."""
colors = ("black", "black")
[docs]
class BaseLegendHandler:
"""Base telescope handler that can handle any telescope type."""
telescope_type = None
def __init__(self, telescope_type=None):
self.telescope_type = self.telescope_type if telescope_type is None else telescope_type
self.config = get_telescope_config(self.telescope_type)
def _create_patch(self, handlebox, x0, y0, size):
"""Create the patch for the configured shape, sized relative to ``size``."""
color = self.config["color"]
transform = handlebox.get_transform()
shape = self.config["shape"]
if shape == "circle":
facecolor = color if self.config["filled"] else "none"
return mpatches.Circle(
xy=(x0, y0),
radius=size * self.config["radius"] / REFERENCE_RADIUS,
facecolor=facecolor,
edgecolor=color,
transform=transform,
)
if shape == "square":
return mpatches.Rectangle(
[x0, y0], size, size, facecolor=color, edgecolor=color, transform=transform
)
if shape == "hexagon":
return mpatches.RegularPolygon(
(x0, y0),
numVertices=6,
radius=0.7 * size * self.config["radius"] / REFERENCE_RADIUS,
orientation=np.deg2rad(30),
facecolor=color,
edgecolor=color,
transform=transform,
)
raise ValueError(f"Unknown shape: {shape}")
[docs]
def legend_artist(self, _, __, ___, handlebox):
"""Create the appropriate patch based on telescope type."""
shape = self.config["shape"]
if shape == "circle":
x0, y0 = calculate_center(handlebox, 4, 2)
elif shape == "square":
x0, y0 = calculate_center(handlebox, 10, 1)
else:
x0, y0 = calculate_center(handlebox)
patch = self._create_patch(handlebox, x0, y0, handlebox.height)
handlebox.add_artist(patch)
return patch
[docs]
class LSTHandler(BaseLegendHandler):
"""Legend handler for LST telescopes."""
telescope_type = "LST"
[docs]
class MSTHandler(BaseLegendHandler):
"""Legend handler for MST telescopes."""
telescope_type = "MST"
[docs]
class SSTHandler(BaseLegendHandler):
"""Legend handler for SST telescopes."""
telescope_type = "SST"
[docs]
class SCTHandler(BaseLegendHandler):
"""Legend handler for SCT telescopes."""
telescope_type = "SCT"
[docs]
class HESSHandler(BaseLegendHandler):
"""Legend handler for HESS telescopes."""
telescope_type = "HESS"
[docs]
class MAGICHandler(BaseLegendHandler):
"""Legend handler for MAGIC telescopes."""
telescope_type = "MAGIC"
[docs]
class VERITASHandler(BaseLegendHandler):
"""Legend handler for VERITAS telescopes."""
telescope_type = "VERITAS"