#!/usr/bin/env python3 """Anki flashcard generator for Warsaw districts. Generates Anki-compatible flashcard decks with maps showing individual Warsaw districts (dzielnice) with their borders using real boundary data from OpenStreetMap. Usage: # Generate Anki cards for all Warsaw districts python -m python_pkg.warsaw_districts.warsaw_districts_anki # Specify custom output file python -m python_pkg.warsaw_districts.warsaw_districts_anki --output warsaw.apkg Output: Creates a self-contained .apkg file that can be directly imported into Anki. The file includes all images embedded, so no manual file copying is needed. """ from __future__ import annotations import argparse import hashlib from io import BytesIO from pathlib import Path import random import sys from typing import TYPE_CHECKING import genanki import geopandas as gpd import matplotlib.pyplot as plt if TYPE_CHECKING: from collections.abc import Sequence from matplotlib.figure import Figure # Path to GeoJSON file with Warsaw district boundaries GEOJSON_PATH = Path(__file__).parent / "warszawa-dzielnice.geojson" def load_district_data() -> gpd.GeoDataFrame: """Load Warsaw district boundaries from GeoJSON. Returns: GeoDataFrame with district boundaries. """ if not GEOJSON_PATH.exists(): msg = f"GeoJSON file not found at {GEOJSON_PATH}" raise FileNotFoundError(msg) gdf = gpd.read_file(GEOJSON_PATH) # Filter out the "Warszawa" entry (whole city) and keep only districts return gdf[gdf["name"] != "Warszawa"].copy() def get_district_names() -> list[str]: """Get list of all district names from GeoJSON data. Returns: Sorted list of district names. """ gdf = load_district_data() return sorted(gdf["name"].tolist()) # Load district names from actual data WARSAW_DISTRICTS = get_district_names() def create_district_map(district_name: str) -> Figure: """Create a map showing Warsaw districts with one district highlighted. Args: district_name: Name of the district to highlight. Returns: A matplotlib Figure object. """ # Load all district data gdf = load_district_data() # Create figure fig, ax = plt.subplots(figsize=(10, 10)) ax.set_aspect("equal") ax.axis("off") # Plot all districts with light gray borders gdf.boundary.plot(ax=ax, color="lightgray", linewidth=0.5, alpha=0.5) # Find and highlight the target district target = gdf[gdf["name"] == district_name] if len(target) == 0: msg = f"District {district_name} not found in data" raise ValueError(msg) # Plot the highlighted district with bold black border target.boundary.plot(ax=ax, color="black", linewidth=3) # Set tight layout ax.set_xlim(gdf.total_bounds[0], gdf.total_bounds[2]) ax.set_ylim(gdf.total_bounds[1], gdf.total_bounds[3]) return fig def generate_district_image_bytes(district_name: str) -> bytes: """Generate a district map image as bytes. Args: district_name: Name of the district to visualize. Returns: PNG image data as bytes. """ fig = create_district_map(district_name) # Save to bytes buffer buf = BytesIO() fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) plt.close(fig) buf.seek(0) return buf.read() def generate_anki_package( deck_name: str = "Warsaw Districts", ) -> genanki.Package: """Generate Anki package (.apkg) for Warsaw districts. Args: deck_name: Name for the Anki deck. Returns: genanki.Package object ready to be written to file. """ # Create a unique model ID based on deck name model_id_hash = hashlib.md5( # noqa: S324 f"warsaw_districts_{deck_name}".encode() ) model_id = int(model_id_hash.hexdigest()[:8], 16) # Define the note model (card template) my_model = genanki.Model( model_id, "Warsaw District Model", fields=[ {"name": "DistrictMap"}, {"name": "DistrictName"}, ], templates=[ { "name": "Card 1", "qfmt": "{{DistrictMap}}", "afmt": '{{FrontSide}}