"""
Situation (point symbol) layer for mappyng.
Renders categorical or uniform point symbols on the map using SVG
primitives, circles, squares, triangles, diamonds, stars, crosses,
custom SVG files, or *composite* markers (combinations of shapes).
Configured through :class:`~mappyng.SituationLayer`.
Example
-------
>>> from mappyng import SituationLayer
>>> m.add(SituationLayer(
... gdf_pts, column="type",
... symbol={
... "Hospital": {"marker": "cross", "fill": "#e63946", "size": 12},
... "School": {"marker": "square", "fill": "#457b9d", "size": 10},
... },
... legend={"title": "Équipements"},
... ))
"""
from __future__ import annotations
import math
import warnings
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import geopandas as gpd
from shapely.geometry import Point, MultiPoint
from . import defaults as D
from .config import Style
from .legend import (
BaseLegendConfig,
LegendFrameConfig,
_parse_frame_config,
compute_legend_position,
draw_legend_title,
draw_legend_subtitle,
draw_legend_frame,
)
from .renderer import SvgViewport, ViewBox
# ============================================================================
# Defaults
# ============================================================================
SITUATION_DEFAULT_MARKER = "circle"
SITUATION_DEFAULT_COLOR = "#e31a1c"
SITUATION_DEFAULT_SIZE = 8.0
SITUATION_DEFAULT_STROKE = "#ffffff"
SITUATION_DEFAULT_STROKE_WIDTH = 0.5
SITUATION_DEFAULT_FILL_OPACITY = 1.0
SITUATION_MISSING_COLOR = "#e63946"
SITUATION_MISSING_MARKER = "cross"
# ============================================================================
# Shape path generators
# ============================================================================
def _circle_path(cx: float, cy: float, r: float) -> str:
"""SVG path for a circle (two arcs)."""
return (
f"M{cx - r:.2f},{cy:.2f}"
f"A{r:.2f},{r:.2f} 0 1,0 {cx + r:.2f},{cy:.2f}"
f"A{r:.2f},{r:.2f} 0 1,0 {cx - r:.2f},{cy:.2f}Z"
)
def _square_path(cx: float, cy: float, r: float) -> str:
"""SVG path for a square inscribed in radius *r*."""
s = r * 0.85 # half-side
return (
f"M{cx - s:.2f},{cy - s:.2f}"
f"L{cx + s:.2f},{cy - s:.2f}"
f"L{cx + s:.2f},{cy + s:.2f}"
f"L{cx - s:.2f},{cy + s:.2f}Z"
)
def _triangle_path(cx: float, cy: float, r: float) -> str:
"""SVG path for an upward-pointing equilateral triangle."""
# Top vertex
tx, ty = cx, cy - r
# Bottom-left and bottom-right
bx = r * math.cos(math.radians(30))
by = r * math.sin(math.radians(30))
return (
f"M{tx:.2f},{ty:.2f}"
f"L{cx + bx:.2f},{cy + by:.2f}"
f"L{cx - bx:.2f},{cy + by:.2f}Z"
)
def _diamond_path(cx: float, cy: float, r: float) -> str:
"""SVG path for a diamond (rotated square)."""
return (
f"M{cx:.2f},{cy - r:.2f}"
f"L{cx + r * 0.7:.2f},{cy:.2f}"
f"L{cx:.2f},{cy + r:.2f}"
f"L{cx - r * 0.7:.2f},{cy:.2f}Z"
)
def _star_path(cx: float, cy: float, r: float) -> str:
"""SVG path for a 5-pointed star."""
points = []
for i in range(10):
angle = math.radians(i * 36 - 90)
rad = r if i % 2 == 0 else r * 0.4
px = cx + rad * math.cos(angle)
py = cy + rad * math.sin(angle)
points.append(f"{px:.2f},{py:.2f}")
return "M" + "L".join(points) + "Z"
def _cross_path(cx: float, cy: float, r: float) -> str:
"""SVG path for a + cross (thick arms)."""
t = r * 0.3 # arm thickness
return (
f"M{cx - t:.2f},{cy - r:.2f}"
f"L{cx + t:.2f},{cy - r:.2f}"
f"L{cx + t:.2f},{cy - t:.2f}"
f"L{cx + r:.2f},{cy - t:.2f}"
f"L{cx + r:.2f},{cy + t:.2f}"
f"L{cx + t:.2f},{cy + t:.2f}"
f"L{cx + t:.2f},{cy + r:.2f}"
f"L{cx - t:.2f},{cy + r:.2f}"
f"L{cx - t:.2f},{cy + t:.2f}"
f"L{cx - r:.2f},{cy + t:.2f}"
f"L{cx - r:.2f},{cy - t:.2f}"
f"L{cx - t:.2f},{cy - t:.2f}Z"
)
def _x_cross_path(cx: float, cy: float, r: float) -> str:
"""SVG path for an X cross (diagonal)."""
t = r * 0.22
d = r * 0.707 # r / sqrt(2)
dt = t * 0.707
# Build the 12-pointed shape of an X
return (
f"M{cx:.2f},{cy - dt:.2f}"
f"L{cx + d - dt:.2f},{cy - d:.2f}"
f"L{cx + d:.2f},{cy - d + dt:.2f}"
f"L{cx + dt:.2f},{cy:.2f}"
f"L{cx + d:.2f},{cy + d - dt:.2f}"
f"L{cx + d - dt:.2f},{cy + d:.2f}"
f"L{cx:.2f},{cy + dt:.2f}"
f"L{cx - d + dt:.2f},{cy + d:.2f}"
f"L{cx - d:.2f},{cy + d - dt:.2f}"
f"L{cx - dt:.2f},{cy:.2f}"
f"L{cx - d:.2f},{cy - d + dt:.2f}"
f"L{cx - d + dt:.2f},{cy - d:.2f}Z"
)
def _pentagon_path(cx: float, cy: float, r: float) -> str:
"""SVG path for a regular pentagon."""
points = []
for i in range(5):
angle = math.radians(i * 72 - 90)
px = cx + r * math.cos(angle)
py = cy + r * math.sin(angle)
points.append(f"{px:.2f},{py:.2f}")
return "M" + "L".join(points) + "Z"
def _hexagon_path(cx: float, cy: float, r: float) -> str:
"""SVG path for a regular hexagon (flat-top)."""
points = []
for i in range(6):
angle = math.radians(i * 60)
px = cx + r * math.cos(angle)
py = cy + r * math.sin(angle)
points.append(f"{px:.2f},{py:.2f}")
return "M" + "L".join(points) + "Z"
_SHAPE_BUILDERS = {
"circle": _circle_path,
"square": _square_path,
"triangle": _triangle_path,
"diamond": _diamond_path,
"star": _star_path,
"cross": _cross_path,
"plus": _cross_path,
"x": _x_cross_path,
"pentagon": _pentagon_path,
"hexagon": _hexagon_path,
}
# ============================================================================
# Composite markers
# ============================================================================
def _parse_marker(marker: Any) -> List[str]:
"""Parse a marker specification into a list of shape names.
Accepts:
- ``"circle"``: single shape
- ``"circle+cross"``: composite (circle behind, cross on top)
- ``["circle", "cross"]``: same as above
- ``"path/to/icon.svg"``: custom SVG (returned as-is in a list)
"""
if isinstance(marker, list):
return marker
if isinstance(marker, str):
if "+" in marker and not marker.endswith(".svg"):
return [m.strip() for m in marker.split("+")]
return [marker]
return [SITUATION_DEFAULT_MARKER]
def _is_svg_file(name: str) -> bool:
"""Check if a marker name refers to an external SVG file."""
return name.endswith(".svg") and "/" in name or name.endswith(".svg")
# ============================================================================
# SVG marker rendering
# ============================================================================
def _load_svg_marker(path: str) -> Optional[str]:
"""Load an SVG file and return its inner content as a string."""
p = Path(path)
if not p.exists():
return None
return p.read_text(encoding="utf-8")
def _draw_marker_on_viewport(
viewport: SvgViewport,
layer: str,
cx: float,
cy: float,
shapes: List[str],
size: float,
fill: str,
stroke: str,
stroke_width: float,
fill_opacity: float,
z_order: int,
elem_id: Optional[str] = None,
inner_fill: Optional[str] = None,
inner_stroke: Optional[str] = None,
) -> None:
"""Draw a (possibly composite) marker at SVG coordinates (cx, cy).
For composite markers, each shape after the first uses ``inner_fill``
(default: stroke color) and ``inner_stroke`` so they stand out.
"""
r = size / 2.0
for i, shape_name in enumerate(shapes):
is_first = (i == 0)
cur_fill = fill if is_first else (inner_fill or stroke)
cur_stroke = stroke if is_first else (inner_stroke or fill)
cur_opacity = str(fill_opacity) if is_first else "1"
attrs: Dict[str, Any] = {
"fill": cur_fill,
"stroke": cur_stroke,
"stroke-width": str(stroke_width),
"fill-opacity": cur_opacity,
}
if elem_id and is_first:
attrs["id"] = elem_id
# Inner shapes are slightly smaller
cur_r = r if is_first else r * 0.55
if shape_name in _SHAPE_BUILDERS:
d = _SHAPE_BUILDERS[shape_name](cx, cy, cur_r)
group = viewport.get_layer(layer, z_order)
elem = ET.SubElement(group, "path", d=d)
for k, v in attrs.items():
elem.set(k, str(v))
elif _is_svg_file(shape_name):
svg_content = _load_svg_marker(shape_name)
if svg_content is None:
# Fallback to circle
d = _circle_path(cx, cy, cur_r)
group = viewport.get_layer(layer, z_order)
elem = ET.SubElement(group, "path", d=d)
for k, v in attrs.items():
elem.set(k, str(v))
else:
_inject_svg_marker(viewport, layer, cx, cy, cur_r * 2,
svg_content, z_order, elem_id if is_first else None)
else:
# Unknown shape, fallback to circle
d = _circle_path(cx, cy, cur_r)
group = viewport.get_layer(layer, z_order)
elem = ET.SubElement(group, "path", d=d)
for k, v in attrs.items():
elem.set(k, str(v))
def _inject_svg_marker(
viewport: SvgViewport,
layer: str,
cx: float,
cy: float,
size: float,
svg_content: str,
z_order: int,
elem_id: Optional[str] = None,
) -> None:
"""Inject a custom SVG marker centered at (cx, cy)."""
group = viewport.get_layer(layer, z_order)
g = ET.SubElement(group, "g")
if elem_id:
g.set("id", elem_id)
# Parse the SVG content to extract viewBox dimensions
try:
svg_root = ET.fromstring(svg_content)
except ET.ParseError:
return
vb = svg_root.get("viewBox", "")
if vb:
parts = vb.split()
if len(parts) == 4:
vb_w, vb_h = float(parts[2]), float(parts[3])
else:
vb_w = vb_h = size
else:
vb_w = float(svg_root.get("width", size))
vb_h = float(svg_root.get("height", size))
# Scale to fit within size
scale = size / max(vb_w, vb_h)
actual_w = vb_w * scale
actual_h = vb_h * scale
# Translate so the marker is centered at (cx, cy)
tx = cx - actual_w / 2
ty = cy - actual_h / 2
g.set("transform", f"translate({tx:.2f},{ty:.2f}) scale({scale:.4f})")
# Append children of the SVG root (skip the <svg> wrapper)
for child in svg_root:
g.append(child)
# ============================================================================
# Legend marker preview (for legend swatches)
# ============================================================================
def _draw_legend_marker(
parent: ET.Element,
cx: float,
cy: float,
shapes: List[str],
size: float,
fill: str,
stroke: str,
stroke_width: float,
fill_opacity: float,
inner_fill: Optional[str] = None,
inner_stroke: Optional[str] = None,
) -> None:
"""Draw a marker preview inside a legend element."""
r = size / 2.0
for i, shape_name in enumerate(shapes):
is_first = (i == 0)
cur_fill = fill if is_first else (inner_fill or stroke)
cur_stroke = stroke if is_first else (inner_stroke or fill)
cur_r = r if is_first else r * 0.55
if shape_name in _SHAPE_BUILDERS:
d = _SHAPE_BUILDERS[shape_name](cx, cy, cur_r)
elem = ET.SubElement(parent, "path", d=d)
elem.set("fill", cur_fill)
elem.set("stroke", cur_stroke)
elem.set("stroke-width", str(stroke_width))
if is_first:
elem.set("fill-opacity", str(fill_opacity))
elif _is_svg_file(shape_name):
# For legend, render a simple circle as placeholder for SVG markers
d = _circle_path(cx, cy, cur_r)
elem = ET.SubElement(parent, "path", d=d)
elem.set("fill", cur_fill)
elem.set("stroke", cur_stroke)
elem.set("stroke-width", str(stroke_width))
# ============================================================================
# Symbol config parsing
# ============================================================================
@dataclass
class MarkerConfig:
"""Resolved configuration for a single marker type."""
shapes: List[str]
fill: str = SITUATION_DEFAULT_COLOR
size: float = SITUATION_DEFAULT_SIZE
stroke: str = SITUATION_DEFAULT_STROKE
stroke_width: float = SITUATION_DEFAULT_STROKE_WIDTH
fill_opacity: float = SITUATION_DEFAULT_FILL_OPACITY
inner_fill: Optional[str] = None
inner_stroke: Optional[str] = None
label: Optional[str] = None
def _parse_symbol_config(raw: Dict[str, Any]) -> MarkerConfig:
"""Parse a single symbol dict into a MarkerConfig."""
marker = raw.get("marker", SITUATION_DEFAULT_MARKER)
shapes = _parse_marker(marker)
return MarkerConfig(
shapes=shapes,
fill=raw.get("fill", SITUATION_DEFAULT_COLOR),
size=raw.get("size", SITUATION_DEFAULT_SIZE),
stroke=raw.get("stroke", SITUATION_DEFAULT_STROKE),
stroke_width=raw.get("stroke_width", SITUATION_DEFAULT_STROKE_WIDTH),
fill_opacity=raw.get("fill_opacity", SITUATION_DEFAULT_FILL_OPACITY),
inner_fill=raw.get("inner_fill", None),
inner_stroke=raw.get("inner_stroke", None),
label=raw.get("label", None),
)
# ============================================================================
# Situation legend
# ============================================================================
[docs]
@dataclass
class SituationLegendConfig(BaseLegendConfig):
"""Legend configuration for situation (categorical marker) layers.
Inherits all shared fields from :class:`~mappyng.legend.BaseLegendConfig`.
Pass as the ``legend`` dict in ``SituationLayer``.
Attributes
----------
marker_size : float
Size of the marker preview in the legend (SVG px, default 10.0).
label_gap : float
Gap between marker preview and label text (SVG px, default 6.0).
row_spacing : float
Vertical gap between legend rows (SVG px, default 4.0).
col_gap : float
Horizontal gap between columns in multi-column layout (SVG px,
default 20.0).
columns_n : int
Number of columns in the legend (default 1).
"""
marker_size: float = 10.0
label_gap: float = 6.0
row_spacing: float = 4.0
col_gap: float = 20.0
"""Horizontal gap between columns in multi-column layout (SVG px)."""
columns_n: int = 1
"""Number of columns in the legend (default 1 = single column)."""
def _draw_situation_legend(
map_obj: Any,
categories: Dict[str, MarkerConfig],
legend_params: Optional[Dict[str, Any]],
layer_id: int,
):
"""Draw a categorical legend with marker previews.
Supports single-column (default) and multi-column (``columns_n > 1``) layouts.
Uses :class:`SituationLegendConfig` for all styling, consistent with
other legend types.
"""
if legend_params is None:
return
if not categories:
return
lp = dict(legend_params)
sit_cfg = SituationLegendConfig(**lp)
sit_cfg.resolve_defaults(map_obj.style, font_scale=getattr(map_obj, "font_scale", 1.0))
frame_cfg = _parse_frame_config(sit_cfg.frame)
font_size = sit_cfg.fontsize or D.LEGEND_FONT_SIZE
ff = sit_cfg.fontfamily or D.FONT_FAMILY
fc = sit_cfg.fontcolor or D.FONT_COLOR
marker_size = sit_cfg.marker_size
label_gap = sit_cfg.label_gap
row_spacing = sit_cfg.row_spacing
col_gap = sit_cfg.col_gap
n_cols = max(1, sit_cfg.columns_n)
svg = map_obj.svg
vb = map_obj.main.viewbox
legend_x, legend_y = compute_legend_position(sit_cfg, vb)
layer_name = f"legend_situation_{layer_id}"
legend_group = svg.add_overlay_group(layer_name, z_order=D.Z_LEGEND)
y_cursor = legend_y
y_cursor = draw_legend_title(legend_group, legend_x, y_cursor, sit_cfg, align="left")
y_cursor = draw_legend_subtitle(legend_group, legend_x, y_cursor, sit_cfg, align="left")
content_top = y_cursor
# Marker slot sized on the largest category marker so the legend
# shows each marker at its on-map size while rows stay aligned.
cat_items = list(categories.items())
slot = max([marker_size] + [mcfg.size for _, mcfg in cat_items])
half_marker = slot / 2.0
row_h = max(slot, font_size) + row_spacing
# Estimate per-column width (used for col offsets and frame)
n_rows = math.ceil(len(cat_items) / n_cols)
max_label_px = max(
(len(mcfg.label or cat) * font_size * 0.6) for cat, mcfg in cat_items
) if cat_items else 100.0
col_width = slot + label_gap + max_label_px + col_gap
for i, (cat_name, mcfg) in enumerate(cat_items):
col_idx = i % n_cols
row_idx = i // n_cols
x0 = legend_x + col_idx * col_width
row_y = content_top + row_idx * row_h + half_marker
# Marker preview
_draw_legend_marker(
legend_group,
x0 + half_marker, row_y,
mcfg.shapes, mcfg.size,
mcfg.fill, mcfg.stroke, mcfg.stroke_width, mcfg.fill_opacity,
inner_fill=mcfg.inner_fill, inner_stroke=mcfg.inner_stroke,
)
# Label
label = mcfg.label or cat_name
label_elem = ET.SubElement(
legend_group, "text",
x=f"{x0 + slot + label_gap:.2f}",
y=f"{row_y + font_size * 0.35:.2f}",
)
label_elem.text = label
label_elem.set("font-size", str(font_size))
label_elem.set("fill", fc)
label_elem.set("font-family", ff)
total_h = n_rows * row_h
content_w = n_cols * col_width - col_gap
content_h = total_h + (content_top - legend_y)
# Frame
if frame_cfg.enabled:
title_offset = (sit_cfg.title_fontsize or D.LEGEND_TITLE_FONT_SIZE) * 0.3
draw_legend_frame(
legend_group, svg._defs,
legend_x, legend_y - title_offset,
content_w, content_h + title_offset,
frame_cfg,
)
return (legend_x, legend_y, legend_x + content_w, legend_y + content_h)
# ============================================================================
# Main drawing function
# ============================================================================
def _get_points(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
"""Extract point coordinates from a GeoDataFrame.
For polygon geometries, uses the centroid.
"""
result = gdf.copy()
geom_types = result.geometry.geom_type.unique()
needs_centroid = any(t in ("Polygon", "MultiPolygon") for t in geom_types)
if needs_centroid:
result = result.copy()
result.geometry = result.geometry.centroid
return result
def add_situation(
map_obj: Any,
gdf: gpd.GeoDataFrame,
*,
column: Optional[str] = None,
symbol: Optional[Dict[str, Dict[str, Any]]] = None,
marker: str = SITUATION_DEFAULT_MARKER,
fill: str = SITUATION_DEFAULT_COLOR,
size: float = SITUATION_DEFAULT_SIZE,
stroke: str = SITUATION_DEFAULT_STROKE,
stroke_width: float = SITUATION_DEFAULT_STROKE_WIDTH,
fill_opacity: float = SITUATION_DEFAULT_FILL_OPACITY,
inner_fill: Optional[str] = None,
inner_stroke: Optional[str] = None,
label: Optional[str] = None,
legend_params: Optional[Dict[str, Any]] = None,
on_zoom: bool = True,
on_cartouches: bool = True,
) -> int:
"""Add a situation (point symbol) layer to the map.
Two modes:
1. **Categorical**: pass ``column`` + ``symbol`` dict mapping each
category value to its marker config.
2. **Uniform**: pass ``marker``, ``fill``, ``size`` etc. directly,
all points get the same symbol.
Returns
-------
int
Layer ID for interactivity tracking.
"""
layer_id = map_obj._next_layer_id("situation")
# Convert polygons to centroids
gdf_pts = _get_points(gdf)
# Build category to MarkerConfig mapping
categories: Dict[str, MarkerConfig] = {}
if column and symbol:
# Categorical mode. The >N-categories readability check lives
# in SituationLayer.validate(), emitted at construction.
for cat_value, sym_dict in symbol.items():
categories[str(cat_value)] = _parse_symbol_config(sym_dict)
else:
# Uniform mode, single marker for all points
shapes = _parse_marker(marker)
default_cfg = MarkerConfig(
shapes=shapes, fill=fill, size=size,
stroke=stroke, stroke_width=stroke_width,
fill_opacity=fill_opacity,
inner_fill=inner_fill, inner_stroke=inner_stroke,
label=label,
)
categories["__all__"] = default_cfg
layer_name = f"situation_{layer_id}"
z = D.Z_SITUATION
id_prefix = f"situ-{layer_id}-"
# --- Render on main viewport ---
_draw_situation_on_viewport(
map_obj.main, gdf_pts, column, categories,
layer_name, z, id_prefix, map_obj.bbox, map_obj,
)
# --- Render on zoom ---
if on_zoom and map_obj._zoom_viewport and map_obj._zoom_bbox:
zoom_gdf = map_obj._clip_gdf(gdf_pts, map_obj._zoom_bbox)
if not zoom_gdf.empty:
_draw_situation_on_viewport(
map_obj._zoom_viewport, zoom_gdf, column, categories,
layer_name, z, None, map_obj._zoom_bbox, map_obj,
)
# --- Render on cartouches ---
if on_cartouches and map_obj.cartouche_params:
for index, vp in map_obj._cartouche_viewports.items():
cart_gdf = map_obj._prepare_cartouche_data(
gdf_pts, map_obj.cartouche_params[index])
if cart_gdf is not None and not cart_gdf.empty:
_draw_situation_on_viewport(
vp, cart_gdf, column, categories,
layer_name, z, None, None, map_obj,
)
# --- Legend ---
if legend_params is not None:
legend_cats = {k: v for k, v in categories.items() if k != "__all__"}
if not legend_cats and "__all__" in categories:
cfg = categories["__all__"]
if cfg.label:
legend_cats = {cfg.label: cfg}
if legend_cats:
legend_bbox = _draw_situation_legend(map_obj, legend_cats, legend_params, layer_id)
if legend_bbox is not None:
map_obj.overflow.register(*legend_bbox)
return layer_id
def _draw_situation_on_viewport(
viewport: SvgViewport,
gdf: gpd.GeoDataFrame,
column: Optional[str],
categories: Dict[str, MarkerConfig],
layer_name: str,
z_order: int,
id_prefix: Optional[str],
bbox: Optional[list],
map_obj: Any,
) -> None:
"""Draw situation markers on a single viewport."""
vb = viewport.viewbox
# Clip to bbox if provided
if bbox is not None:
from shapely.geometry import box as geo_box
clip_geom = geo_box(*bbox)
try:
gdf = gdf.clip(mask=clip_geom)
gdf = gdf[~gdf.is_empty]
except Exception:
pass
if gdf.empty:
return
for idx, row in gdf.iterrows():
geom = row.geometry
if geom is None or geom.is_empty:
continue
# Determine category
if column and column in row.index:
cat = str(row[column])
else:
cat = "__all__"
if cat in categories:
mcfg = categories[cat]
elif "__all__" in categories:
mcfg = categories["__all__"]
else:
# Missing category, render with warning marker
mcfg = MarkerConfig(
shapes=_parse_marker(SITUATION_MISSING_MARKER),
fill=SITUATION_MISSING_COLOR,
size=SITUATION_DEFAULT_SIZE,
stroke="#ffffff",
stroke_width=SITUATION_DEFAULT_STROKE_WIDTH,
fill_opacity=1.0,
)
# Get point coordinates
points = []
if isinstance(geom, Point):
points.append((geom.x, geom.y))
elif isinstance(geom, MultiPoint):
for pt in geom.geoms:
points.append((pt.x, pt.y))
for px, py in points:
cx, cy = vb.geo_to_svg(px, py)
eid = f"{id_prefix}{idx}" if id_prefix else None
_draw_marker_on_viewport(
viewport, layer_name, cx, cy,
mcfg.shapes, mcfg.size, mcfg.fill, mcfg.stroke,
mcfg.stroke_width, mcfg.fill_opacity, z_order,
elem_id=eid,
inner_fill=mcfg.inner_fill,
inner_stroke=mcfg.inner_stroke,
)