Spaces:
Running
Running
Download src/maptoposter/models.py from isaachwf/MapToPoster: direct link, hf CLI and curl.
- Browser
- Download file 7.36 kB
-
https://huggingface.co/spaces/isaachwf/MapToPoster/resolve/main/src/maptoposter/models.py
- Command line
-
hf download hf://spaces/isaachwf/MapToPoster/src/maptoposter/models.py
-
curl -L -o models.py https://huggingface.co/spaces/isaachwf/MapToPoster/resolve/main/src/maptoposter/models.py
7.36 kB
| """Typed configuration and data models for MapToPoster.""" | |
| from __future__ import annotations | |
| import math | |
| from dataclasses import dataclass, field, fields, replace | |
| from enum import StrEnum | |
| from pathlib import Path | |
| class LayoutPreset(StrEnum): | |
| CLASSIC = "classic" | |
| EDITORIAL = "editorial" | |
| MINIMAL = "minimal" | |
| BOTTOM_LEFT = "bottom_left" | |
| CENTERED = "centered" | |
| class SizePreset(StrEnum): | |
| THREE_FOUR = "3:4" | |
| FOUR_FIVE = "4:5" | |
| TWO_THREE = "2:3" | |
| FOUR_THREE = "4:3" | |
| FIVE_FOUR = "5:4" | |
| THREE_TWO = "3:2" | |
| SQUARE = "1:1" | |
| NINE_SIXTEEN = "9:16" | |
| SIXTEEN_NINE = "16:9" | |
| A4 = "A4" | |
| A3 = "A3" | |
| A4_LANDSCAPE = "A4-landscape" | |
| A3_LANDSCAPE = "A3-landscape" | |
| CUSTOM = "custom" | |
| class OutputFormat(StrEnum): | |
| PNG = "png" | |
| SVG = "svg" | |
| PDF = "pdf" | |
| class Coordinate: | |
| latitude: float | |
| longitude: float | |
| def __post_init__(self) -> None: | |
| if not math.isfinite(self.latitude) or not -90 <= self.latitude <= 90: | |
| raise ValueError("Latitude must be a finite value between -90 and 90.") | |
| if not math.isfinite(self.longitude) or not -180 <= self.longitude <= 180: | |
| raise ValueError("Longitude must be a finite value between -180 and 180.") | |
| class BBox: | |
| west: float | |
| south: float | |
| east: float | |
| north: float | |
| def __post_init__(self) -> None: | |
| values = (self.west, self.south, self.east, self.north) | |
| if not all(math.isfinite(value) for value in values): | |
| raise ValueError("Bounding box values must be finite.") | |
| if not -90 <= self.south < self.north <= 90: | |
| raise ValueError("Bounding box latitude values are invalid.") | |
| if not -180 <= self.west < self.east <= 180: | |
| raise ValueError("Bounding boxes crossing the antimeridian are not supported.") | |
| def as_tuple(self) -> tuple[float, float, float, float]: | |
| """Return the OSMnx 2.x bbox order: west, south, east, north.""" | |
| return (self.west, self.south, self.east, self.north) | |
| class Location: | |
| display_name: str | |
| coordinate: Coordinate | |
| country: str = "" | |
| region: str = "" | |
| country_code: str = "" | |
| provider: str = "manual" | |
| class MapViewport: | |
| center: Coordinate | |
| bbox: BBox | |
| zoom: float | None = None | |
| distance_m: float | None = None | |
| class MapConfig: | |
| viewport: MapViewport | |
| network_type: str = "all" | |
| data_layers: tuple[str, ...] = ("roads", "water", "parks") | |
| def __post_init__(self) -> None: | |
| if self.network_type not in {"all", "drive", "walk", "bike"}: | |
| raise ValueError(f"Unsupported network type: {self.network_type}") | |
| class LayerConfig: | |
| motorway: bool = True | |
| primary: bool = True | |
| secondary: bool = True | |
| residential: bool = True | |
| water: bool = True | |
| parks: bool = True | |
| class StyleConfig: | |
| id: str | |
| name: str | |
| description: str = "" | |
| preview: str = "" | |
| background: str = "#FFFFFF" | |
| text: str = "#000000" | |
| water: str = "#C0C0C0" | |
| parks: str = "#F0F0F0" | |
| road_motorway: str = "#0A0A0A" | |
| road_primary: str = "#1A1A1A" | |
| road_secondary: str = "#2A2A2A" | |
| road_tertiary: str = "#3A3A3A" | |
| road_residential: str = "#4A4A4A" | |
| road_default: str = "#3A3A3A" | |
| gradient: str = "#FFFFFF" | |
| def with_colors(self, **colors: str) -> StyleConfig: | |
| """Return a customized style without mutating its preset.""" | |
| allowed = {item.name for item in fields(self)} - {"id", "name", "description", "preview"} | |
| unknown = set(colors) - allowed | |
| if unknown: | |
| raise ValueError(f"Unknown style colors: {', '.join(sorted(unknown))}") | |
| return replace(self, **colors) | |
| class TypographyConfig: | |
| title: str = "" | |
| subtitle: str = "" | |
| caption: str = "" | |
| coordinates: str = "" | |
| font_family: str = "auto" | |
| title_size: float = 54 | |
| subtitle_size: float = 22 | |
| caption_size: float = 14 | |
| coordinate_size: float = 12 | |
| letter_spacing: float = 0.08 | |
| line_height: float = 1.15 | |
| alignment: str = "center" | |
| show_coordinates: bool = True | |
| show_divider: bool = True | |
| def __post_init__(self) -> None: | |
| if self.alignment not in {"left", "center", "right"}: | |
| raise ValueError("Typography alignment must be left, center, or right.") | |
| sizes = (self.title_size, self.subtitle_size, self.caption_size, self.coordinate_size) | |
| if not all(6 <= size <= 240 for size in sizes): | |
| raise ValueError("Typography sizes must be between 6 and 240 points.") | |
| SIZE_PRESETS: dict[SizePreset, tuple[float, float]] = { | |
| SizePreset.THREE_FOUR: (12.0, 16.0), | |
| SizePreset.FOUR_FIVE: (12.0, 15.0), | |
| SizePreset.TWO_THREE: (12.0, 18.0), | |
| SizePreset.FOUR_THREE: (16.0, 12.0), | |
| SizePreset.FIVE_FOUR: (15.0, 12.0), | |
| SizePreset.THREE_TWO: (18.0, 12.0), | |
| SizePreset.SQUARE: (12.0, 12.0), | |
| SizePreset.NINE_SIXTEEN: (9.0, 16.0), | |
| SizePreset.SIXTEEN_NINE: (16.0, 9.0), | |
| SizePreset.A4: (8.2677, 11.6929), | |
| SizePreset.A3: (11.6929, 16.5354), | |
| SizePreset.A4_LANDSCAPE: (11.6929, 8.2677), | |
| SizePreset.A3_LANDSCAPE: (16.5354, 11.6929), | |
| } | |
| class PosterSize: | |
| preset: SizePreset = SizePreset.THREE_FOUR | |
| width_in: float | None = None | |
| height_in: float | None = None | |
| def __post_init__(self) -> None: | |
| if self.preset is SizePreset.CUSTOM: | |
| if self.width_in is None or self.height_in is None: | |
| raise ValueError("Custom poster size requires width and height.") | |
| if not 4 <= self.width_in <= 40 or not 4 <= self.height_in <= 40: | |
| raise ValueError("Custom poster dimensions must be between 4 and 40 inches.") | |
| def dimensions(self) -> tuple[float, float]: | |
| if self.preset is SizePreset.CUSTOM: | |
| assert self.width_in is not None and self.height_in is not None | |
| return self.width_in, self.height_in | |
| return SIZE_PRESETS[self.preset] | |
| class LayoutConfig: | |
| preset: LayoutPreset = LayoutPreset.CLASSIC | |
| class PosterConfig: | |
| location: Location | |
| map: MapConfig | |
| style: StyleConfig | |
| typography: TypographyConfig = field(default_factory=TypographyConfig) | |
| layout: LayoutConfig = field(default_factory=LayoutConfig) | |
| layers: LayerConfig = field(default_factory=LayerConfig) | |
| size: PosterSize = field(default_factory=PosterSize) | |
| class PreviewConfig: | |
| dpi: int = 120 | |
| max_dimension_px: int = 1200 | |
| class ExportConfig: | |
| output_format: OutputFormat = OutputFormat.PNG | |
| dpi: int = 300 | |
| output_path: Path | None = None | |
| def __post_init__(self) -> None: | |
| if not 72 <= self.dpi <= 600: | |
| raise ValueError("Export DPI must be between 72 and 600.") | |
| class MapDataRef: | |
| cache_key: str | |
| cache_path: Path | |
| cache_hit: bool | |
| class MapData: | |
| bbox: BBox | |
| roads: object | |
| water_polygons: object | None = None | |
| water_lines: object | None = None | |
| parks: object | None = None | |
| crs: str = "EPSG:4326" | |
| metadata: dict[str, object] = field(default_factory=dict) | |