diff --git a/python_pkg/anki_decks/__init__.py b/python_pkg/anki_decks/__init__.py new file mode 100644 index 0000000..62a27d6 --- /dev/null +++ b/python_pkg/anki_decks/__init__.py @@ -0,0 +1 @@ +"""Anki flashcard deck generators.""" diff --git a/python_pkg/anki_decks/polish_coastal_features/__init__.py b/python_pkg/anki_decks/polish_coastal_features/__init__.py new file mode 100644 index 0000000..e290be8 --- /dev/null +++ b/python_pkg/anki_decks/polish_coastal_features/__init__.py @@ -0,0 +1 @@ +"""Polish coastal features Anki generator.""" diff --git a/python_pkg/anki_decks/polish_coastal_features/polish_coastal_features_anki.py b/python_pkg/anki_decks/polish_coastal_features/polish_coastal_features_anki.py new file mode 100644 index 0000000..f85a41c --- /dev/null +++ b/python_pkg/anki_decks/polish_coastal_features/polish_coastal_features_anki.py @@ -0,0 +1,333 @@ +"""Anki flashcard generator for Polish coastal features. + +Generates Anki-compatible flashcard decks with maps showing coastal features +(peninsulas, cliffs, beaches, etc.) highlighted on a Poland map. +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt +from shapely.geometry import LineString, MultiLineString, MultiPolygon, Polygon + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_coastal_features + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +FEATURE_COLOR_POLYGON = "#D4AC0D" # Gold for polygon features +FEATURE_COLOR_LINE = "#D4AC0D" # Gold for line features +LINE_WIDTH = 4 + + +def create_coastal_map( + feature_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Poland with one coastal feature highlighted.""" + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Plot the feature based on geometry type + geom = feature_gdf.iloc[0].geometry + if isinstance(geom, Polygon | MultiPolygon): + feature_gdf.plot(ax=ax, color=FEATURE_COLOR_POLYGON, alpha=0.9) + feature_gdf.boundary.plot(ax=ax, color="#1A1A1A", linewidth=3) + elif isinstance(geom, LineString | MultiLineString): + feature_gdf.plot(ax=ax, color=FEATURE_COLOR_LINE, linewidth=LINE_WIDTH) + + # Set bounds to Poland + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_coastal_image_bytes( + feature_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> bytes: + """Generate a coastal feature map image as bytes.""" + fig = create_coastal_map(feature_gdf, poland_boundary) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None + + +def _init_worker(poland_geojson: str) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + + +def _render_single_feature(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single feature image (worker function). + + Args: + args: Tuple of (feature_name, feature_geojson_str). + + Returns: + Tuple of (feature_name, image_bytes). + """ + feature_name, feature_geojson = args + feature_gdf = gpd.read_file(feature_geojson) + + assert _mp_poland_boundary is not None # noqa: S101 + + image_data = generate_coastal_image_bytes(feature_gdf, _mp_poland_boundary) + return feature_name, image_data + + +def generate_anki_package( + features: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish Coastal Features", +) -> genanki.Package: + """Generate Anki package for Polish coastal features.""" + model_id_hash = hashlib.md5( # noqa: S324 + f"polish_coastal_features_{deck_name}".encode() + ) + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.info-text { + font-size: 18px; + color: #7F8C8D; + margin-top: 10px; +} +.card.night_mode .info-text { + color: #BDC3C7; +} +""" + + my_model = genanki.Model( + model_id, + "Polish Coastal Feature Model", + fields=[ + {"name": "FeatureMap"}, + {"name": "FeatureName"}, + {"name": "FeatureType"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{FeatureMap}}
', + "afmt": '
{{FeatureMap}}
' + '
' + '
{{FeatureName}}
' + '
{{FeatureType}}
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Prepare data for parallel processing + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (feature_name, feature_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in features.iterrows(): + feature_gdf = gpd.GeoDataFrame([row], crs=features.crs) + feature_geojson = feature_gdf.to_json() + work_items.append((row["name"], feature_geojson)) + + # Use multiprocessing for parallel rendering + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path,), + ) as pool: + for i, (feature_name, image_data) in enumerate( + pool.imap_unordered(_render_single_feature, work_items) + ): + results[feature_name] = image_data + if (i + 1) % 10 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in features.iterrows(): + feature_name = row["name"] + feature_type = row.get("type", "coastal feature") + image_data = results[feature_name] + filename = f"coastal_{feature_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', feature_name, feature_type], + tags=["geography", "poland", "coastal", "baltic"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish coastal features.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_coastal_features.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish Coastal Features", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = ( + Path(args.output) if args.output else Path("polish_coastal_features.apkg") + ) + + try: + sys.stdout.write("Loading coastal features data...\n") + features = get_polish_coastal_features() + poland_boundary = get_poland_boundary() + num_features = len(features) + + sys.stdout.write(f"Found {num_features} coastal features.\n") + sys.stdout.write("Generating flashcards...\n") + + package = generate_anki_package(features, poland_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_features = list(features.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_features)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_features: + feature_name = row["name"] + feature_gdf = gpd.GeoDataFrame([row], crs=features.crs) + image_data = generate_coastal_image_bytes(feature_gdf, poland_boundary) + safe_name = feature_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Coastal features: {num_features}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_coastal_features/run.sh b/python_pkg/anki_decks/polish_coastal_features/run.sh new file mode 100755 index 0000000..f0c24b3 --- /dev/null +++ b/python_pkg/anki_decks/polish_coastal_features/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run the Polish coastal features Anki generator + +cd "$(dirname "$0")" || exit + +python polish_coastal_features_anki.py --preview preview_images --preview-count 5 "$@" diff --git a/python_pkg/anki_decks/polish_forests/__init__.py b/python_pkg/anki_decks/polish_forests/__init__.py new file mode 100644 index 0000000..d659891 --- /dev/null +++ b/python_pkg/anki_decks/polish_forests/__init__.py @@ -0,0 +1 @@ +"""Polish forests Anki generator.""" diff --git a/python_pkg/anki_decks/polish_forests/polish_forests_anki.py b/python_pkg/anki_decks/polish_forests/polish_forests_anki.py new file mode 100644 index 0000000..2a186ec --- /dev/null +++ b/python_pkg/anki_decks/polish_forests/polish_forests_anki.py @@ -0,0 +1,322 @@ +"""Anki flashcard generator for Polish forests (puszcze). + +Generates Anki-compatible flashcard decks with maps showing large forests +highlighted on a Poland map. +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_forests + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +FOREST_COLOR = "#1D4E2B" # Dark green for forests + + +def create_forest_map( + forest_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Poland with one forest highlighted.""" + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Plot the forest + forest_gdf.plot(ax=ax, color=FOREST_COLOR, alpha=0.9) + forest_gdf.boundary.plot(ax=ax, color="#1A1A1A", linewidth=1.5) + + # Set bounds to Poland + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_forest_image_bytes( + forest_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> bytes: + """Generate a forest map image as bytes.""" + fig = create_forest_map(forest_gdf, poland_boundary) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None + + +def _init_worker(poland_geojson: str) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + + +def _render_single_forest(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single forest image (worker function). + + Args: + args: Tuple of (forest_name, forest_geojson_str). + + Returns: + Tuple of (forest_name, image_bytes). + """ + forest_name, forest_geojson = args + forest_gdf = gpd.read_file(forest_geojson) + + assert _mp_poland_boundary is not None # noqa: S101 + + image_data = generate_forest_image_bytes(forest_gdf, _mp_poland_boundary) + return forest_name, image_data + + +def generate_anki_package( + forests: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish Forests (Puszcze)", +) -> genanki.Package: + """Generate Anki package for Polish forests.""" + model_id_hash = hashlib.md5(f"polish_forests_{deck_name}".encode()) # noqa: S324 + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.info-text { + font-size: 18px; + color: #7F8C8D; + margin-top: 10px; +} +.card.night_mode .info-text { + color: #BDC3C7; +} +""" + + my_model = genanki.Model( + model_id, + "Polish Forest Model", + fields=[ + {"name": "ForestMap"}, + {"name": "ForestName"}, + {"name": "Area"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{ForestMap}}
', + "afmt": '
{{ForestMap}}
' + '
' + '
{{ForestName}}
' + '
{{Area}} km²
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Prepare data for parallel processing + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (forest_name, forest_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in forests.iterrows(): + forest_gdf = gpd.GeoDataFrame([row], crs=forests.crs) + forest_geojson = forest_gdf.to_json() + work_items.append((row["name"], forest_geojson)) + + # Use multiprocessing for parallel rendering + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path,), + ) as pool: + for i, (forest_name, image_data) in enumerate( + pool.imap_unordered(_render_single_forest, work_items) + ): + results[forest_name] = image_data + if (i + 1) % 10 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in forests.iterrows(): + forest_name = row["name"] + area_km2 = round(row["area_km2"], 1) if "area_km2" in row else 0 + image_data = results[forest_name] + filename = f"forest_{forest_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', forest_name, str(area_km2)], + tags=["geography", "poland", "forests", "puszcza", "nature"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish forests.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_forests.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish Forests (Puszcze)", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("polish_forests.apkg") + + try: + sys.stdout.write("Loading forests data...\n") + forests = get_polish_forests() + poland_boundary = get_poland_boundary() + num_forests = len(forests) + + sys.stdout.write(f"Found {num_forests} forests.\n") + sys.stdout.write("Generating flashcards...\n") + + package = generate_anki_package(forests, poland_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_forests = list(forests.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_forests)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_forests: + forest_name = row["name"] + forest_gdf = gpd.GeoDataFrame([row], crs=forests.crs) + image_data = generate_forest_image_bytes(forest_gdf, poland_boundary) + safe_name = forest_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Forests: {num_forests}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_forests/run.sh b/python_pkg/anki_decks/polish_forests/run.sh new file mode 100755 index 0000000..3a81296 --- /dev/null +++ b/python_pkg/anki_decks/polish_forests/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run the Polish forests Anki generator + +cd "$(dirname "$0")" || exit + +python polish_forests_anki.py --preview preview_images --preview-count 5 "$@" diff --git a/python_pkg/anki_decks/polish_gminy/__init__.py b/python_pkg/anki_decks/polish_gminy/__init__.py new file mode 100644 index 0000000..d4273f1 --- /dev/null +++ b/python_pkg/anki_decks/polish_gminy/__init__.py @@ -0,0 +1 @@ +"""Polish gminy (municipalities) Anki flashcard generator.""" diff --git a/python_pkg/anki_decks/polish_gminy/polish_gminy_anki.py b/python_pkg/anki_decks/polish_gminy/polish_gminy_anki.py new file mode 100755 index 0000000..3ceaeae --- /dev/null +++ b/python_pkg/anki_decks/polish_gminy/polish_gminy_anki.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python3 +"""Anki flashcard generator for Polish gminy (municipalities). + +Generates Anki-compatible flashcard decks with maps showing individual +Polish municipalities highlighted on a country map. + +Uses multiprocessing to parallelize image generation for ~4x speedup. +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_gminy + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +# 2500 colors for gminy (cycling through) +GMINA_COLORS = [ + "#E74C3C", + "#3498DB", + "#2ECC71", + "#9B59B6", + "#F39C12", + "#1ABC9C", + "#E91E63", + "#00BCD4", + "#8BC34A", + "#FF5722", + "#673AB7", + "#FFEB3B", + "#795548", + "#607D8B", + "#CDDC39", + "#FF9800", + "#4CAF50", + "#03A9F4", + "#F44336", + "#009688", + "#3F51B5", + "#FFC107", + "#9E9E9E", + "#00E676", + "#FF4081", + "#448AFF", + "#69F0AE", + "#FFD740", + "#40C4FF", + "#B388FF", + "#EA80FC", + "#82B1FF", + "#A7FFEB", + "#FFFF8D", + "#FF80AB", + "#536DFE", + "#64FFDA", + "#FFE57F", + "#80D8FF", + "#B9F6CA", + "#CF6679", + "#BB86FC", + "#03DAC6", + "#018786", + "#6200EE", + "#3700B3", + "#B00020", + "#FF0266", + "#C51162", + "#AA00FF", +] + + +def create_gmina_map( + gmina_name: str, + gmina_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + color_map: dict[str, str], +) -> Figure: + """Create a map showing Poland with one gmina highlighted.""" + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Get pre-computed color + fill_color = color_map.get(gmina_name, GMINA_COLORS[0]) + + # Plot the highlighted gmina + gmina_gdf.plot(ax=ax, color=fill_color, alpha=0.9) + gmina_gdf.boundary.plot(ax=ax, color="#1A1A1A", linewidth=1.5) + + # Set bounds to Poland + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_gmina_image_bytes( + gmina_name: str, + gmina_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + color_map: dict[str, str], +) -> bytes: + """Generate a gmina map image as bytes.""" + fig = create_gmina_map(gmina_name, gmina_gdf, poland_boundary, color_map) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +def _build_color_map(names: list[str]) -> dict[str, str]: + """Pre-compute color mapping for all names. + + Args: + names: List of all gmina names. + + Returns: + Dictionary mapping name to color. + """ + sorted_names = sorted(names) + return { + name: GMINA_COLORS[i % len(GMINA_COLORS)] for i, name in enumerate(sorted_names) + } + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None +_mp_color_map: dict[str, str] | None = None + + +def _init_worker( + poland_geojson: str, + color_map: dict[str, str], +) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary, _mp_color_map # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + _mp_color_map = color_map + + +def _render_single_gmina(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single gmina image (worker function). + + Args: + args: Tuple of (gmina_name, gmina_geojson_str). + + Returns: + Tuple of (gmina_name, image_bytes). + """ + gmina_name, gmina_geojson = args + gmina_gdf = gpd.read_file(gmina_geojson) + + assert _mp_poland_boundary is not None # noqa: S101 + assert _mp_color_map is not None # noqa: S101 + + image_data = generate_gmina_image_bytes( + gmina_name, gmina_gdf, _mp_poland_boundary, _mp_color_map + ) + return gmina_name, image_data + + +def generate_anki_package( + gminy: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish Gminy", +) -> genanki.Package: + """Generate Anki package for Polish gminy.""" + model_id_hash = hashlib.md5(f"polish_gminy_{deck_name}".encode()) # noqa: S324 + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +""" + + my_model = genanki.Model( + model_id, + "Polish Gmina Model", + fields=[ + {"name": "GminaMap"}, + {"name": "GminaName"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{GminaMap}}
', + "afmt": '
{{GminaMap}}
' + '
' + '
{{GminaName}}
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Pre-compute color mapping once (avoids O(n²) sorting) + color_map = _build_color_map(gminy["name"].tolist()) + + # Prepare data for parallel processing + # Serialize GeoDataFrames to GeoJSON strings for pickling + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (gmina_name, gmina_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in gminy.iterrows(): + gmina_gdf = gpd.GeoDataFrame([row], crs=gminy.crs) + gmina_geojson = gmina_gdf.to_json() + work_items.append((row["name"], gmina_geojson)) + + # Use multiprocessing for parallel rendering + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path, color_map), + ) as pool: + for i, (gmina_name, image_data) in enumerate( + pool.imap_unordered(_render_single_gmina, work_items) + ): + results[gmina_name] = image_data + if (i + 1) % 100 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in gminy.iterrows(): + gmina_name = row["name"] + image_data = results[gmina_name] + filename = f"gmina_{gmina_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', gmina_name], + tags=["geography", "poland", "gminy"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish gminy.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_gminy.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish Gminy", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("polish_gminy.apkg") + + try: + sys.stdout.write("Loading gminy data...\n") + gminy = get_polish_gminy() + poland_boundary = get_poland_boundary() + num_gminy = len(gminy) + + sys.stdout.write(f"Generating flashcards for {num_gminy} gminy...\n") + sys.stdout.write("This will take a while for ~2500 gminy...\n") + + package = generate_anki_package(gminy, poland_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_gminy = list(gminy.iterrows())[: args.preview_count] + # Pre-compute color mapping for previews + color_map = _build_color_map(gminy["name"].tolist()) + sys.stdout.write( + f"Exporting {len(preview_gminy)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_gminy: + gmina_name = row["name"] + gmina_gdf = gpd.GeoDataFrame([row], crs=gminy.crs) + image_data = generate_gmina_image_bytes( + gmina_name, gmina_gdf, poland_boundary, color_map + ) + safe_name = gmina_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Gminy: {num_gminy}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_gminy/run.sh b/python_pkg/anki_decks/polish_gminy/run.sh new file mode 100755 index 0000000..ab83b32 --- /dev/null +++ b/python_pkg/anki_decks/polish_gminy/run.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Script to generate Polish Gminy Anki deck +# WARNING: This will take a long time (~2500 gminy) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="$SCRIPT_DIR/.venv" +PREVIEW_DIR="$SCRIPT_DIR/preview_images" + +echo "=== Polish Gminy Anki Generator ===" +echo "WARNING: This may take a very long time (fetching ~2500 gminy)" +echo + +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment..." + python3 -m venv "$VENV_DIR" +fi + +echo "Activating virtual environment..." +source "$VENV_DIR/bin/activate" + +echo "Installing dependencies..." +pip install --quiet --upgrade pip +pip install --quiet matplotlib genanki geopandas requests shapely + +cd "$SCRIPT_DIR" + +# Create preview images directory +mkdir -p "$PREVIEW_DIR" + +python -m polish_gminy_anki --output polish_gminy.apkg --preview "$PREVIEW_DIR" --preview-count 5 + +echo +echo "Done! The Anki deck is at: $SCRIPT_DIR/polish_gminy.apkg" +echo "Preview images are in: $PREVIEW_DIR" diff --git a/python_pkg/anki_decks/polish_islands/__init__.py b/python_pkg/anki_decks/polish_islands/__init__.py new file mode 100644 index 0000000..183c452 --- /dev/null +++ b/python_pkg/anki_decks/polish_islands/__init__.py @@ -0,0 +1 @@ +"""Polish islands Anki generator.""" diff --git a/python_pkg/anki_decks/polish_islands/polish_islands_anki.py b/python_pkg/anki_decks/polish_islands/polish_islands_anki.py new file mode 100644 index 0000000..8285930 --- /dev/null +++ b/python_pkg/anki_decks/polish_islands/polish_islands_anki.py @@ -0,0 +1,410 @@ +"""Anki flashcard generator for Polish islands. + +Generates Anki-compatible flashcard decks with maps showing islands +highlighted on a Poland map. +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_islands + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +ISLAND_COLOR = "#E67E22" # Orange for islands +NEIGHBOR_COLOR = "#EAECEE" # Lighter gray for extended view + +# Padding for zoom (in degrees) +ZOOM_PADDING_DEG = 0.2 + + +def _island_extends_beyond( + island_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> bool: + """Check if island extends beyond Poland's boundaries.""" + poland_bounds = poland_boundary.total_bounds # [minx, miny, maxx, maxy] + island_bounds = island_gdf.total_bounds + + # Check if any part of island is outside Poland + extends_west = island_bounds[0] < poland_bounds[0] + extends_south = island_bounds[1] < poland_bounds[1] + extends_east = island_bounds[2] > poland_bounds[2] + extends_north = island_bounds[3] > poland_bounds[3] + + return extends_west or extends_south or extends_east or extends_north + + +def create_island_map( + island_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + *, + zoom: bool, +) -> Figure: + """Create a map showing Poland with one island highlighted. + + Args: + island_gdf: GeoDataFrame with the island to highlight. + poland_boundary: GeoDataFrame with Poland's boundary. + zoom: If True, zoom to island area for better visibility. + """ + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + extends_beyond = _island_extends_beyond(island_gdf, poland_boundary) + + if extends_beyond: + # Draw extended background if island goes beyond Poland + island_bounds = island_gdf.total_bounds + padding = 0.5 + ax.fill( + [ + island_bounds[0] - padding, + island_bounds[2] + padding, + island_bounds[2] + padding, + island_bounds[0] - padding, + ], + [ + island_bounds[1] - padding, + island_bounds[1] - padding, + island_bounds[3] + padding, + island_bounds[3] + padding, + ], + color=NEIGHBOR_COLOR, + zorder=0, + ) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Plot the island with thinner lines + island_gdf.plot(ax=ax, color=ISLAND_COLOR, alpha=0.9) + island_gdf.boundary.plot(ax=ax, color="#1A1A1A", linewidth=1.5) + + # Set bounds based on zoom mode and whether island extends beyond + if zoom: + # Zoom to island area with padding + island_bounds = island_gdf.total_bounds + ax.set_xlim( + island_bounds[0] - ZOOM_PADDING_DEG, + island_bounds[2] + ZOOM_PADDING_DEG, + ) + ax.set_ylim( + island_bounds[1] - ZOOM_PADDING_DEG, + island_bounds[3] + ZOOM_PADDING_DEG, + ) + elif extends_beyond: + # Include the full island in view + island_bounds = island_gdf.total_bounds + poland_bounds = poland_boundary.total_bounds + ax.set_xlim( + min(poland_bounds[0], island_bounds[0] - 0.1), + max(poland_bounds[2], island_bounds[2] + 0.1), + ) + ax.set_ylim( + min(poland_bounds[1], island_bounds[1] - 0.1), + max(poland_bounds[3], island_bounds[3] + 0.1), + ) + else: + # Normal Poland bounds + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_island_image_bytes( + island_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + *, + zoom: bool, +) -> bytes: + """Generate an island map image as bytes.""" + fig = create_island_map(island_gdf, poland_boundary, zoom=zoom) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None +_mp_zoom_mode: str = "no-zoom" + + +def _init_worker(poland_geojson: str, zoom_mode: str) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary, _mp_zoom_mode # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + _mp_zoom_mode = zoom_mode + + +def _render_single_island(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single island image (worker function). + + Args: + args: Tuple of (island_name, island_geojson_str). + + Returns: + Tuple of (island_name, image_bytes). + """ + island_name, island_geojson = args + island_gdf = gpd.read_file(island_geojson) + + assert _mp_poland_boundary is not None # noqa: S101 + + image_data = generate_island_image_bytes( + island_gdf, _mp_poland_boundary, zoom=(_mp_zoom_mode == "zoom") + ) + return island_name, image_data + + +def generate_anki_package( + islands: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish Islands", + *, + zoom: bool = True, +) -> genanki.Package: + """Generate Anki package for Polish islands.""" + model_id_hash = hashlib.md5(f"polish_islands_{deck_name}".encode()) # noqa: S324 + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.info-text { + font-size: 18px; + color: #7F8C8D; + margin-top: 10px; +} +.card.night_mode .info-text { + color: #BDC3C7; +} +""" + + my_model = genanki.Model( + model_id, + "Polish Island Model", + fields=[ + {"name": "IslandMap"}, + {"name": "IslandName"}, + {"name": "Area"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{IslandMap}}
', + "afmt": '
{{IslandMap}}
' + '
' + '
{{IslandName}}
' + '
{{Area}} km²
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Prepare data for parallel processing + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (island_name, island_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in islands.iterrows(): + island_gdf = gpd.GeoDataFrame([row], crs=islands.crs) + island_geojson = island_gdf.to_json() + work_items.append((row["name"], island_geojson)) + + # Use multiprocessing for parallel rendering + zoom_mode = "zoom" if zoom else "no-zoom" + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path, zoom_mode), + ) as pool: + for i, (island_name, image_data) in enumerate( + pool.imap_unordered(_render_single_island, work_items) + ): + results[island_name] = image_data + if (i + 1) % 10 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in islands.iterrows(): + island_name = row["name"] + area_km2 = round(row["area_km2"], 1) if "area_km2" in row else 0 + image_data = results[island_name] + filename = f"island_{island_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', island_name, str(area_km2)], + tags=["geography", "poland", "islands", "coastal"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish islands.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_islands.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish Islands", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("polish_islands.apkg") + + try: + sys.stdout.write("Loading islands data...\n") + islands = get_polish_islands() + poland_boundary = get_poland_boundary() + num_islands = len(islands) + + sys.stdout.write(f"Found {num_islands} islands.\n") + sys.stdout.write("Generating flashcards...\n") + + package = generate_anki_package(islands, poland_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_islands = list(islands.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_islands)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_islands: + island_name = row["name"] + island_gdf = gpd.GeoDataFrame([row], crs=islands.crs) + image_data = generate_island_image_bytes( + island_gdf, poland_boundary, zoom=True + ) + safe_name = island_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Islands: {num_islands}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_islands/run.sh b/python_pkg/anki_decks/polish_islands/run.sh new file mode 100755 index 0000000..3994ee3 --- /dev/null +++ b/python_pkg/anki_decks/polish_islands/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run the Polish islands Anki generator + +cd "$(dirname "$0")" || exit + +python polish_islands_anki.py --preview preview_images --preview-count 5 "$@" diff --git a/python_pkg/anki_decks/polish_lakes/__init__.py b/python_pkg/anki_decks/polish_lakes/__init__.py new file mode 100644 index 0000000..e50af49 --- /dev/null +++ b/python_pkg/anki_decks/polish_lakes/__init__.py @@ -0,0 +1 @@ +"""Polish lakes Anki deck generator.""" diff --git a/python_pkg/anki_decks/polish_lakes/polish_lakes_anki.py b/python_pkg/anki_decks/polish_lakes/polish_lakes_anki.py new file mode 100644 index 0000000..279fa57 --- /dev/null +++ b/python_pkg/anki_decks/polish_lakes/polish_lakes_anki.py @@ -0,0 +1,362 @@ +"""Anki flashcard generator for Polish lakes. + +Generates Anki-compatible flashcard decks with ZOOMED maps showing lakes +highlighted for better visibility. +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_lakes + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +LAKE_COLOR = "#3498DB" # Blue for lakes +ZOOM_PADDING_DEG = 0.3 # Degrees of padding around lake for zoomed view + + +def create_lake_map( + lake_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + *, + zoom: bool = True, +) -> Figure: + """Create a map showing Poland with one lake highlighted.""" + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Plot the lake with thinner border + lake_gdf.plot(ax=ax, color=LAKE_COLOR, alpha=0.9) + lake_gdf.boundary.plot(ax=ax, color="#1A1A1A", linewidth=1.5) + + if zoom: + # Zoom to lake area with padding + bounds = lake_gdf.total_bounds + min_x, min_y, max_x, max_y = bounds + # Add padding + ax.set_xlim(min_x - ZOOM_PADDING_DEG, max_x + ZOOM_PADDING_DEG) + ax.set_ylim(min_y - ZOOM_PADDING_DEG, max_y + ZOOM_PADDING_DEG) + else: + # Set bounds to Poland + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_lake_image_bytes( + lake_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + *, + zoom: bool = True, +) -> bytes: + """Generate a lake map image as bytes.""" + fig = create_lake_map(lake_gdf, poland_boundary, zoom=zoom) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None +_mp_zoom: bool = True + + +def _init_worker(poland_geojson: str, zoom_mode: str) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary, _mp_zoom # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + _mp_zoom = zoom_mode == "zoom" + + +def _render_single_lake(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single lake image (worker function). + + Args: + args: Tuple of (lake_name, lake_geojson_str). + + Returns: + Tuple of (lake_name, image_bytes). + """ + lake_name, lake_geojson = args + lake_gdf = gpd.read_file(lake_geojson) + + assert _mp_poland_boundary is not None # noqa: S101 + + image_data = generate_lake_image_bytes(lake_gdf, _mp_poland_boundary, zoom=_mp_zoom) + return lake_name, image_data + + +def generate_anki_package( + lakes: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish Lakes", + *, + zoom: bool = True, +) -> genanki.Package: + """Generate Anki package for Polish lakes.""" + model_id_hash = hashlib.md5(f"polish_lakes_{deck_name}".encode()) # noqa: S324 + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.info-text { + font-size: 18px; + color: #7F8C8D; + margin-top: 10px; +} +.card.night_mode .info-text { + color: #BDC3C7; +} +""" + + my_model = genanki.Model( + model_id, + "Polish Lake Model", + fields=[ + {"name": "LakeMap"}, + {"name": "LakeName"}, + {"name": "Area"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{LakeMap}}
', + "afmt": '
{{LakeMap}}
' + '
' + '
{{LakeName}}
' + '
{{Area}} km²
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Prepare data for parallel processing + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (lake_name, lake_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in lakes.iterrows(): + lake_gdf = gpd.GeoDataFrame([row], crs=lakes.crs) + lake_geojson = lake_gdf.to_json() + work_items.append((row["name"], lake_geojson)) + + # Use multiprocessing for parallel rendering + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path, "zoom" if zoom else "no-zoom"), + ) as pool: + for i, (lake_name, image_data) in enumerate( + pool.imap_unordered(_render_single_lake, work_items) + ): + results[lake_name] = image_data + if (i + 1) % 50 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in lakes.iterrows(): + lake_name = row["name"] + area_km2 = round(row["area_km2"], 1) if "area_km2" in row else 0 + image_data = results[lake_name] + filename = f"lake_{lake_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', lake_name, str(area_km2)], + tags=["geography", "poland", "lakes", "water"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish lakes.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_lakes.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish Lakes", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + parser.add_argument( + "--no-zoom", + action="store_true", + help="Disable zoom (show entire Poland instead of zoomed region)", + ) + parser.add_argument( + "--limit", + "-l", + type=int, + default=None, + help="Limit number of lakes (for testing)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("polish_lakes.apkg") + zoom = not args.no_zoom + + try: + sys.stdout.write("Loading lakes data...\n") + lakes = get_polish_lakes() + poland_boundary = get_poland_boundary() + + if args.limit: + lakes = lakes.head(args.limit) + sys.stdout.write(f"Limiting to {args.limit} lakes.\n") + + num_lakes = len(lakes) + + sys.stdout.write(f"Found {num_lakes} lakes.\n") + sys.stdout.write(f"Zoom mode: {'enabled' if zoom else 'disabled'}\n") + sys.stdout.write("Generating flashcards...\n") + + package = generate_anki_package( + lakes, poland_boundary, args.deck_name, zoom=zoom + ) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_lakes = list(lakes.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_lakes)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_lakes: + lake_name = row["name"] + lake_gdf = gpd.GeoDataFrame([row], crs=lakes.crs) + image_data = generate_lake_image_bytes( + lake_gdf, poland_boundary, zoom=zoom + ) + safe_name = lake_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Lakes: {num_lakes}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_lakes/run.sh b/python_pkg/anki_decks/polish_lakes/run.sh new file mode 100755 index 0000000..8bfeb08 --- /dev/null +++ b/python_pkg/anki_decks/polish_lakes/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run the Polish lakes Anki generator + +cd "$(dirname "$0")" || exit + +python polish_lakes_anki.py --preview preview_images --preview-count 5 "$@" diff --git a/python_pkg/anki_decks/polish_landscape_parks/__init__.py b/python_pkg/anki_decks/polish_landscape_parks/__init__.py new file mode 100644 index 0000000..d065b03 --- /dev/null +++ b/python_pkg/anki_decks/polish_landscape_parks/__init__.py @@ -0,0 +1 @@ +"""Polish landscape parks Anki deck generator.""" diff --git a/python_pkg/anki_decks/polish_landscape_parks/polish_landscape_parks_anki.py b/python_pkg/anki_decks/polish_landscape_parks/polish_landscape_parks_anki.py new file mode 100644 index 0000000..325ca2f --- /dev/null +++ b/python_pkg/anki_decks/polish_landscape_parks/polish_landscape_parks_anki.py @@ -0,0 +1,336 @@ +"""Anki flashcard generator for Polish landscape parks. + +Generates Anki-compatible flashcard decks with maps showing landscape parks +highlighted on a Poland map. +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_landscape_parks + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +PARK_COLOR = "#27AE60" # Green for landscape parks + + +def create_park_map( + park_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Poland with one landscape park highlighted. + + Clips park geometry to Poland boundary for clean edges. + """ + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Clip park geometry to Poland boundary for clean edges + boundary_union = poland_boundary.union_all() + clipped_gdf = park_gdf.copy() + clipped_gdf["geometry"] = park_gdf.geometry.intersection(boundary_union) + + # Plot the landscape park with thinner lines + clipped_gdf.plot(ax=ax, color=PARK_COLOR, alpha=0.9) + clipped_gdf.boundary.plot(ax=ax, color="#1A1A1A", linewidth=1.5) + + # Set bounds to Poland + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_park_image_bytes( + park_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> bytes: + """Generate a park map image as bytes.""" + fig = create_park_map(park_gdf, poland_boundary) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None + + +def _init_worker(poland_geojson: str) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + + +def _render_single_park(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single park image (worker function). + + Args: + args: Tuple of (park_name, park_geojson_str). + + Returns: + Tuple of (park_name, image_bytes). + """ + park_name, park_geojson = args + park_gdf = gpd.read_file(park_geojson) + # Fix any geometry issues from serialization + park_gdf["geometry"] = park_gdf.geometry.make_valid() + + assert _mp_poland_boundary is not None # noqa: S101 + + image_data = generate_park_image_bytes(park_gdf, _mp_poland_boundary) + return park_name, image_data + + +def generate_anki_package( + parks: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish Landscape Parks", +) -> genanki.Package: + """Generate Anki package for Polish landscape parks.""" + model_id_hash = hashlib.md5( # noqa: S324 + f"polish_landscape_parks_{deck_name}".encode() + ) + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.info-text { + font-size: 18px; + color: #7F8C8D; + margin-top: 10px; +} +.card.night_mode .info-text { + color: #BDC3C7; +} +""" + + my_model = genanki.Model( + model_id, + "Polish Landscape Park Model", + fields=[ + {"name": "ParkMap"}, + {"name": "ParkName"}, + {"name": "Area"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{ParkMap}}
', + "afmt": '
{{ParkMap}}
' + '
' + '
{{ParkName}}
' + '
{{Area}} km²
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Prepare data for parallel processing + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (park_name, park_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in parks.iterrows(): + park_gdf = gpd.GeoDataFrame([row], crs=parks.crs) + park_geojson = park_gdf.to_json() + work_items.append((row["name"], park_geojson)) + + # Use multiprocessing for parallel rendering + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path,), + ) as pool: + for i, (park_name, image_data) in enumerate( + pool.imap_unordered(_render_single_park, work_items) + ): + results[park_name] = image_data + if (i + 1) % 25 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in parks.iterrows(): + park_name = row["name"] + area_km2 = round(row["area_km2"], 1) if "area_km2" in row else 0 + image_data = results[park_name] + filename = f"lpark_{park_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', park_name, str(area_km2)], + tags=["geography", "poland", "landscape-parks", "nature"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish landscape parks.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_landscape_parks.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish Landscape Parks", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = ( + Path(args.output) if args.output else Path("polish_landscape_parks.apkg") + ) + + try: + sys.stdout.write("Loading landscape parks data...\n") + parks = get_polish_landscape_parks() + poland_boundary = get_poland_boundary() + num_parks = len(parks) + + sys.stdout.write(f"Found {num_parks} landscape parks.\n") + sys.stdout.write("Generating flashcards...\n") + + package = generate_anki_package(parks, poland_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_parks = list(parks.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_parks)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_parks: + park_name = row["name"] + park_gdf = gpd.GeoDataFrame([row], crs=parks.crs) + image_data = generate_park_image_bytes(park_gdf, poland_boundary) + safe_name = park_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Landscape parks: {num_parks}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_landscape_parks/run.sh b/python_pkg/anki_decks/polish_landscape_parks/run.sh new file mode 100755 index 0000000..7867103 --- /dev/null +++ b/python_pkg/anki_decks/polish_landscape_parks/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run the Polish landscape parks Anki generator + +cd "$(dirname "$0")" || exit + +python polish_landscape_parks_anki.py --preview preview_images --preview-count 5 "$@" diff --git a/python_pkg/anki_decks/polish_license_plates/README.md b/python_pkg/anki_decks/polish_license_plates/README.md new file mode 100644 index 0000000..e8cd271 --- /dev/null +++ b/python_pkg/anki_decks/polish_license_plates/README.md @@ -0,0 +1,151 @@ +# Polish License Plates Anki Generator + +Generate Anki flashcards for learning Polish car license plate codes. + +## Overview + +This package generates Anki-compatible flashcard decks for all Polish vehicle registration plate codes. Each code is mapped to its corresponding location (city or powiat). + +Polish license plates use a system where: + +- First letter indicates the **voivodeship** (province) +- Following 1-2 letters indicate the specific **city** or **powiat** (county) + +## Features + +- **444 license plate codes** covering all Polish voivodeships, cities, and powiats +- **Bidirectional flashcards**: + - Code → Location (e.g., `WY` → `Warszawa Wola`) + - Location → Code (e.g., `Warszawa Wola` → `WY`) +- **888 total flashcards** for comprehensive learning +- Visual license plate styling in flashcards (yellow background, monospace font) +- Dark mode support +- Self-contained `.apkg` file - no manual setup required + +## Data Source + +License plate data is automatically extracted from Wikipedia's authoritative table: + +- **Source**: [Vehicle registration plates of Poland](https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_Poland) +- **Update**: Run `python -m python_pkg.anki_decks.polish_license_plates.fetch_license_plates` to refresh data + +This ensures the codes are always based on the most current public information. + +## Usage + +### Generate Flashcards + +```bash +# Generate with default settings +python -m python_pkg.anki_decks.polish_license_plates.polish_license_plates_anki + +# Specify custom output file +python -m python_pkg.anki_decks.polish_license_plates.polish_license_plates_anki \ + --output my_plates.apkg + +# Use custom deck name +python -m python_pkg.anki_decks.polish_license_plates.polish_license_plates_anki \ + --deck-name "My Polish Plates" +``` + +### Update License Plate Data + +To fetch the latest data from Wikipedia: + +```bash +# Use cached data if available (default) +python -m python_pkg.anki_decks.polish_license_plates.fetch_license_plates + +# Force refresh from Wikipedia (ignore cache) +python -m python_pkg.anki_decks.polish_license_plates.fetch_license_plates --force +``` + +**Caching**: Downloaded Wikipedia data is cached for 7 days in `.wikipedia_cache/` to avoid unnecessary requests. Use `--force` to bypass the cache. + +This will update `license_plate_data.py` with the current codes from Wikipedia. + +**Requirements**: `pip install requests beautifulsoup4 lxml` + +### Import into Anki + +1. Open Anki +2. File → Import +3. Select the generated `.apkg` file +4. Click Import + +## Examples + +### License Plate Codes by Voivodeship + +| Voivodeship | First Letter | Example Codes | +| ------------------- | ------------ | ------------------------------------------------ | +| Dolnośląskie | D | DA (Wrocław), DB (Wałbrzych), DJ (Jelenia Góra) | +| Kujawsko-Pomorskie | C | CB (Bydgoszcz), CT (Toruń), CG (Grudziądz) | +| Lubelskie | L | LL (Lublin), LC (Chełm), LZ (Zamość) | +| Lubuskie | F | FZ (Zielona Góra), FG (Gorzów Wielkopolski) | +| Łódzkie | E | ED (Łódź), EP (Piotrków Trybunalski) | +| Małopolskie | K | KR (Kraków), KT (Tarnów), KN (Nowy Sącz) | +| Mazowieckie | W | WA-WZ (Warsaw), WR (Radom), WS (Siedlce) | +| Opolskie | O | OP (Opole), OK (Kędzierzyn-Koźle) | +| Podkarpackie | R | RR (Rzeszów), RP (Przemyśl), RK (Krosno) | +| Podlaskie | B | BI (Białystok), BL (Łomża), BSU (Suwałki) | +| Pomorskie | G | GD (Gdańsk), GDY (Gdynia), GS (Słupsk) | +| Śląskie | S | SK (Katowice), SC (Chorzów), SB (Bielsko-Biała) | +| Świętokrzyskie | T | TK (Kielce), TSK (Skarżysko-Kamienna) | +| Warmińsko-Mazurskie | N | NO (Olsztyn), NE (Elbląg), NG (Giżycko) | +| Wielkopolskie | P | PO (Poznań), PKA (Kalisz), PIA (Piła) | +| Zachodniopomorskie | Z | ZS (Szczecin), ZKO (Koszalin), ZSW (Świnoujście) | + +### Warsaw (Warszawa) Codes + +Warsaw has an extensive range of codes (WA-WZ): + +- WA: Warszawa (general) +- WB: Warszawa Bemowo +- WC: Ciechanów +- WD: Warszawa Praga Południe +- WE: Warszawa Praga Północ +- WH: Warszawa Mokotów +- WY: Warszawa Wola +- And many more... + +## Data + +The package includes 444 license plate codes covering: + +- All 16 Polish voivodeships +- Major cities with powiat rights (e.g., Kraków, Gdańsk, Poznań) +- All powiats (counties) across Poland + +## Testing + +Run the test suite: + +```bash +python -m pytest python_pkg/polish_license_plates/tests/ -v +``` + +All 17 tests validate: + +- Data integrity (444 codes, no duplicates) +- Correct voivodeship prefixes +- Major cities present +- Anki package generation +- Bidirectional card templates +- CLI functionality + +## Technical Details + +- **Package format**: Anki `.apkg` (SQLite database) +- **Card model**: Bidirectional with two templates per note +- **Styling**: Custom CSS with license plate visual design +- **Tags**: `geography`, `poland`, `license-plates`, `transportation` + +## Requirements + +- Python 3.10+ +- genanki (for Anki package generation) + +## License + +Part of the testsAndMisc repository. diff --git a/python_pkg/anki_decks/polish_license_plates/__init__.py b/python_pkg/anki_decks/polish_license_plates/__init__.py new file mode 100644 index 0000000..91b0043 --- /dev/null +++ b/python_pkg/anki_decks/polish_license_plates/__init__.py @@ -0,0 +1,9 @@ +"""Polish license plate Anki flashcard generator.""" + +from __future__ import annotations + +__all__ = ["LICENSE_PLATE_CODES"] + +from python_pkg.anki_decks.polish_license_plates.license_plate_data import ( + LICENSE_PLATE_CODES, +) diff --git a/python_pkg/anki_decks/polish_license_plates/fetch_license_plates.py b/python_pkg/anki_decks/polish_license_plates/fetch_license_plates.py new file mode 100755 index 0000000..7f8fccb --- /dev/null +++ b/python_pkg/anki_decks/polish_license_plates/fetch_license_plates.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +"""Fetch Polish license plate codes from Wikipedia. + +This script scrapes the Wikipedia page "Vehicle registration plates of Poland" +to extract the official license plate codes and their corresponding locations. + +The data is extracted from the wikitable on the page and saved to license_plate_data.py. + +Caching: + Fetched Wikipedia HTML is cached to avoid unnecessary requests. + Cache location: .wikipedia_cache/license_plates.html + Cache expires after 7 days by default. + +Usage: + python -m python_pkg.anki_decks.polish_license_plates.fetch_license_plates + + # Force refresh (ignore cache) + python -m python_pkg.anki_decks.polish_license_plates.fetch_license_plates --force + +Source: + https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_Poland + +Note: + This script requires internet access and the following packages: + - requests + - beautifulsoup4 + - lxml +""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import re +import sys +import time + +try: + from bs4 import BeautifulSoup + import requests +except ImportError: + sys.stderr.write( + "Error: Required packages not installed.\n" + "Install with: pip install requests beautifulsoup4 lxml\n" + ) + sys.exit(1) + +# Constants +MIN_TABLE_COLUMNS = 2 # Minimum columns needed to extract code and location +MAX_CODE_LENGTH = 4 # Maximum length for a valid license plate code +CACHE_EXPIRY_DAYS = 7 # Cache expires after 7 days +USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" # Updated to recent version +) + + +def get_cache_path() -> Path: + """Get the path to the cache file. + + Returns: + Path to the cache file. + """ + script_dir = Path(__file__).parent + cache_dir = script_dir / ".wikipedia_cache" + cache_dir.mkdir(exist_ok=True) + return cache_dir / "license_plates.html" + + +def is_cache_valid(cache_path: Path, max_age_days: int = CACHE_EXPIRY_DAYS) -> bool: + """Check if the cache file exists and is not expired. + + Args: + cache_path: Path to the cache file. + max_age_days: Maximum age in days before cache is considered expired. + + Returns: + True if cache is valid, False otherwise. + """ + if not cache_path.exists(): + return False + + # Check age + file_age_seconds = time.time() - cache_path.stat().st_mtime + max_age_seconds = max_age_days * 24 * 60 * 60 + + return file_age_seconds < max_age_seconds + + +def fetch_wikipedia_html(*, force_refresh: bool = False) -> str: + """Fetch Wikipedia HTML, using cache if available. + + Args: + force_refresh: If True, ignore cache and fetch fresh data. + + Returns: + HTML content of the Wikipedia page. + + Raises: + RuntimeError: If the page cannot be fetched. + """ + cache_path = get_cache_path() + + # Check if we can use cache + if not force_refresh and is_cache_valid(cache_path): + try: + sys.stdout.write(f"Using cached data from {cache_path}\n") + cache_age_hours = int((time.time() - cache_path.stat().st_mtime) / 3600) + sys.stdout.write(f"Cache age: {cache_age_hours} hours\n") + return cache_path.read_text(encoding="utf-8") + except OSError as e: + sys.stderr.write(f"Warning: Failed to read cache: {e}\n") + sys.stderr.write("Fetching fresh data from Wikipedia...\n") + # Fall through to fetch from Wikipedia + + # Fetch from Wikipedia + url = "https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_Poland" + headers = {"User-Agent": USER_AGENT} + + if force_refresh: + sys.stdout.write("Force refresh: Ignoring cache\n") + + sys.stdout.write(f"Fetching data from {url}...\n") + + try: + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + except requests.RequestException as e: + msg = f"Failed to fetch Wikipedia page: {e}" + raise RuntimeError(msg) from e + + # Cache the response + try: + cache_path.write_text(response.text, encoding="utf-8") + sys.stdout.write(f"Cached response to {cache_path}\n") + except OSError as e: + sys.stderr.write(f"Warning: Failed to write cache: {e}\n") + # Continue anyway - the data was fetched successfully + + return response.text + + +def parse_license_plates_from_html(html_content: str) -> dict[str, str]: + """Parse license plate codes from Wikipedia HTML. + + Args: + html_content: HTML content of the Wikipedia page. + + Returns: + Dictionary mapping license plate codes to their locations. + + Raises: + RuntimeError: If no valid tables are found. + """ + soup = BeautifulSoup(html_content, "html.parser") + + # Find all wikitables + tables = soup.find_all("table", {"class": "wikitable"}) + + if not tables: + msg = "No wikitable found on the page" + raise RuntimeError(msg) + + sys.stdout.write(f"Found {len(tables)} tables on the page\n") + + license_plates: dict[str, str] = {} + + # Process each table + for table_idx, table in enumerate(tables): + rows = table.find_all("tr") + + sys.stdout.write(f"Processing table {table_idx + 1} with {len(rows)} rows...\n") + + for row in rows[1:]: # Skip header row + cells = row.find_all(["td", "th"]) + + if len(cells) >= MIN_TABLE_COLUMNS: + # Extract code and location + code_text = cells[0].get_text(strip=True) + location_text = cells[1].get_text(strip=True) + + # Clean up the code (remove spaces, keep only letters) + code = re.sub(r"[^A-Z]", "", code_text.upper()) + + # Skip if code is invalid + if not code or len(code) > MAX_CODE_LENGTH: + continue + + # Clean up location text (remove citations, extra spaces) + location = re.sub(r"\[[0-9]+\]", "", location_text) + location = " ".join(location.split()) + + if location: + license_plates[code] = location + + sys.stdout.write(f"Extracted {len(license_plates)} license plate codes\n") + + return license_plates + + +def fetch_wikipedia_license_plates(*, force_refresh: bool = False) -> dict[str, str]: + """Fetch Polish license plate codes from Wikipedia. + + Args: + force_refresh: If True, ignore cache and fetch fresh data. + + Returns: + Dictionary mapping license plate codes to their locations. + + Raises: + RuntimeError: If the page cannot be fetched or parsed. + """ + html_content = fetch_wikipedia_html(force_refresh=force_refresh) + return parse_license_plates_from_html(html_content) + + +def generate_license_plate_data_file( + license_plates: dict[str, str], + output_path: Path, +) -> None: + """Generate license_plate_data.py file with the extracted data. + + Args: + license_plates: Dictionary mapping codes to locations. + output_path: Path to the output file. + """ + # Group by first letter (voivodeship) + voivodeships: dict[str, list[tuple[str, str]]] = {} + for code, location in sorted(license_plates.items()): + first_letter = code[0] + if first_letter not in voivodeships: + voivodeships[first_letter] = [] + voivodeships[first_letter].append((code, location)) + + # Voivodeship names + voivodeship_names = { + "B": "Podlaskie", + "C": "Kujawsko-Pomorskie", + "D": "Dolnośląskie", + "E": "Łódzkie", + "F": "Lubuskie", + "G": "Pomorskie", + "K": "Małopolskie", + "L": "Lubelskie", + "N": "Warmińsko-Mazurskie", + "O": "Opolskie", + "P": "Wielkopolskie", + "R": "Podkarpackie", + "S": "Śląskie", + "T": "Świętokrzyskie", + "W": "Mazowieckie", + "Z": "Zachodniopomorskie", + } + + # Generate file content + content = '''"""Database of Polish car license plate registration codes. + +This module contains a comprehensive mapping of Polish vehicle registration +plate codes to their corresponding locations (cities, powiats, voivodeships). + +Polish license plates use a system where: +- First letter indicates the voivodeship (province) +- Following 1-2 letters indicate the specific city or powiat (county) + +The database is organized by voivodeships in alphabetical order: +- B: Podlaskie +- C: Kujawsko-Pomorskie +- D: Dolnośląskie +- E: Łódzkie +- F: Lubuskie +- G: Pomorskie +- K: Małopolskie +- L: Lubelskie +- N: Warmińsko-Mazurskie +- O: Opolskie +- P: Wielkopolskie +- R: Podkarpackie +- S: Śląskie +- T: Świętokrzyskie +- W: Mazowieckie +- Z: Zachodniopomorskie + +Data source: + Wikipedia - Vehicle registration plates of Poland + https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_Poland + +Auto-generated by: + python -m python_pkg.anki_decks.polish_license_plates.fetch_license_plates + +Examples: + WA = Warszawa (Warsaw) + KR = Kraków + GD = Gdańsk +""" + +from __future__ import annotations + +LICENSE_PLATE_CODES: dict[str, str] = { +''' + + # Add entries grouped by voivodeship + for letter in sorted(voivodeships.keys()): + voivodeship_name = voivodeship_names.get(letter, f"Voivodeship {letter}") + codes = voivodeships[letter] + + content += f" # {letter} - {voivodeship_name} ({len(codes)} codes)\n" + + for code, location in codes: + # Escape quotes in location + location_escaped = location.replace('"', '\\"') + content += f' "{code}": "{location_escaped}",\n' + + content += "\n" + + # Remove last comma and newline, then close the dict + content = content.rstrip(",\n") + "\n}\n" + + # Write to file + output_path.write_text(content, encoding="utf-8") + sys.stdout.write(f"Generated {output_path}\n") + + +def main() -> int: + """Main entry point. + + Returns: + Exit code. + """ + parser = argparse.ArgumentParser( + description="Fetch Polish license plate codes from Wikipedia.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--force", + "-f", + action="store_true", + help="Force refresh: ignore cache and fetch fresh data from Wikipedia", + ) + + args = parser.parse_args() + + try: + # Fetch data from Wikipedia + license_plates = fetch_wikipedia_license_plates(force_refresh=args.force) + + # Determine output path + script_dir = Path(__file__).parent + output_path = script_dir / "license_plate_data.py" + + # Generate the file + generate_license_plate_data_file(license_plates, output_path) + + sys.stdout.write("\n") + sys.stdout.write("=" * 70 + "\n") + sys.stdout.write("LICENSE PLATE DATA UPDATE COMPLETE\n") + sys.stdout.write("=" * 70 + "\n") + sys.stdout.write(f"Total codes: {len(license_plates)}\n") + sys.stdout.write(f"Output file: {output_path}\n") + sys.stdout.write("\n") + sys.stdout.write("Data source: Wikipedia\n") + sys.stdout.write( + "URL: https://en.wikipedia.org/wiki/" + "Vehicle_registration_plates_of_Poland\n" + ) + sys.stdout.write(f"Cache location: {get_cache_path()}\n") + sys.stdout.write(f"Cache expiry: {CACHE_EXPIRY_DAYS} days\n") + sys.stdout.write("\n") + sys.stdout.write("Next steps:\n") + sys.stdout.write(" 1. Review the generated file\n") + sys.stdout.write( + " 2. Run tests: " + "pytest python_pkg/anki_decks/" + "polish_license_plates/tests/\n" + ) + sys.stdout.write( + " 3. Regenerate Anki package: " + "python -m python_pkg.anki_decks." + "polish_license_plates." + "polish_license_plates_anki\n" + ) + + except RuntimeError as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_license_plates/license_plate_data.py b/python_pkg/anki_decks/polish_license_plates/license_plate_data.py new file mode 100644 index 0000000..bee7157 --- /dev/null +++ b/python_pkg/anki_decks/polish_license_plates/license_plate_data.py @@ -0,0 +1,489 @@ +"""Database of Polish car license plate registration codes. + +This module contains a comprehensive mapping of Polish vehicle registration +plate codes to their corresponding locations (cities, powiats, voivodeships). + +Polish license plates use a system where: +- First letter indicates the voivodeship (province) +- Following 1-2 letters indicate the specific city or powiat (county) + +The database is organized by voivodeships in alphabetical order: +- B: Podlaskie +- C: Kujawsko-Pomorskie +- D: Dolnośląskie +- E: Łódzkie +- F: Lubuskie +- G: Pomorskie +- K: Małopolskie +- L: Lubelskie +- N: Warmińsko-Mazurskie +- O: Opolskie +- P: Wielkopolskie +- R: Podkarpackie +- S: Śląskie +- T: Świętokrzyskie +- W: Mazowieckie +- Z: Zachodniopomorskie + +Data source: + Wikipedia - Vehicle registration plates of Poland + https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_Poland + +Note: + This data can be automatically updated by running: + python -m python_pkg.anki_decks.polish_license_plates.fetch_license_plates + +Examples: + WA = Warszawa (Warsaw) + KR = Kraków + GD = Gdańsk +""" + +from __future__ import annotations + +LICENSE_PLATE_CODES: dict[str, str] = { + "DA": "Wrocław Fabryczna", + "DB": "Wałbrzych", + "DC": "Wrocław Śródmieście", + "DD": "Dzierżoniów", + "DE": "Wrocław Psie Pole", + "DF": "Wrocław Krzyki", + "DG": "Głogów", + "DH": "Wrocław Stare Miasto", + "DJ": "Jelenia Góra", + "DK": "Kłodzko", + "DL": "Legnica", + "DLB": "Lubań", + "DLE": "Legnica powiat", + "DMI": "Milicz", + "DN": "Wrocław Nowy Dwór", + "DO": "Oława", + "DP": "Polkowice", + "DR": "Wrocław Krzyki", + "DS": "Świdnica", + "DSR": "Środa Śląska", + "DSW": "Świebodzice", + "DT": "Twardogóra", + "DTR": "Trzebnica", + "DW": "Wałbrzych powiat", + "DWL": "Wołów", + "DWR": "Wrocław", + "DZ": "Zgorzelec", + "DZA": "Ząbkowice Śląskie", + "DZG": "Zgorzelec powiat", + "CB": "Bydgoszcz", + "CBR": "Brodnica", + "CC": "Chełmno", + "CD": "Świecie", + "CE": "Inowrocław", + "CG": "Grudziądz", + "CH": "Chojnice", + "CI": "Inowrocław powiat", + "CL": "Lipno", + "CMG": "Mogilno", + "CN": "Nakło nad Notecią", + "CR": "Radziejów", + "CT": "Toruń", + "CTR": "Toruń powiat", + "CTU": "Tuchola", + "CW": "Włocławek", + "CWA": "Wąbrzeźno", + "CWL": "Włocławek powiat", + "CZ": "Żnin", + "LB": "Biała Podlaska", + "LBI": "Biłgoraj", + "LC": "Chełm", + "LCH": "Chełm powiat", + "LHR": "Hrubieszów", + "LI": "Janów Lubelski", + "LKR": "Kraśnik", + "LKS": "Krasnystaw", + "LL": "Lublin", + "LLE": "Łęczna", + "LLU": "Łuków", + "LM": "Biała Podlaska powiat", + "LOP": "Opole Lubelskie", + "LPA": "Parczew", + "LPU": "Puławy", + "LRA": "Radzyń Podlaski", + "LRY": "Ryki", + "LSI": "Świdnik", + "LT": "Tomaszów Lubelski", + "LU": "Lublin powiat", + "LWL": "Włodawa", + "LZ": "Zamość", + "LZA": "Zamość powiat", + "FG": "Gorzów Wielkopolski", + "FKR": "Krosno Odrzańskie", + "FMI": "Międzyrzecz", + "FNW": "Nowa Sól", + "FSD": "Strzelce-Drezdenko", + "FSL": "Słubice", + "FSU": "Sulęcin", + "FSW": "Świebodzin", + "FWS": "Wschowa", + "FZ": "Zielona Góra", + "FZG": "Zielona Góra powiat", + "FZI": "Żagań", + "FZY": "Żary", + "EA": "Bełchatów", + "EB": "Łódź Bałuty", + "EBE": "Bełchatów powiat", + "EBR": "Brzeziny", + "EC": "Łęczyca", + "ED": "Łódź Śródmieście", + "EE": "Łódź Górna", + "EG": "Głowno", + "EK": "Kutno", + "EKU": "Kutno powiat", + "EL": "Łask", + "ELA": "Łowicz", + "ELE": "Łęczyca powiat", + "ELW": "Łowicz powiat", + "EM": "Opoczno", + "EO": "Opoczno powiat", + "EP": "Piotrków Trybunalski", + "EPA": "Pajęczno", + "EPD": "Poddębice", + "EPI": "Piotrków Trybunalski powiat", + "ER": "Rawa Mazowiecka", + "ERA": "Radomsko", + "ERW": "Rawa Mazowiecka powiat", + "ES": "Sieradz", + "ESI": "Sieradz powiat", + "ESK": "Skierniewice", + "ESR": "Skierniewice powiat", + "ET": "Tomaszów Mazowiecki", + "EW": "Wieluń", + "EWI": "Wieluń powiat", + "EZ": "Zduńska Wola", + "EZD": "Zgierz", + "KA": "Kraków Krowodrza", + "KB": "Bochnia", + "KBC": "Brzesko", + "KC": "Chrzanów", + "KCH": "Chrzanów powiat", + "KD": "Kraków Nowa Huta", + "KDA": "Dąbrowa Tarnowska", + "KE": "Kraków Śródmieście", + "KG": "Gorlice", + "KH": "Kraków Podgórze", + "KI": "Miechów", + "KK": "Kraków Śródmieście", + "KL": "Limanowa", + "KLI": "Limanowa powiat", + "KM": "Myślenice", + "KN": "Nowy Sącz", + "KNS": "Nowy Sącz powiat", + "KNT": "Nowy Targ", + "KO": "Olkusz", + "KOL": "Olkusz powiat", + "KOS": "Oświęcim", + "KP": "Proszowice", + "KR": "Kraków", + "KRA": "Kraków powiat", + "KS": "Sucha Beskidzka", + "KT": "Tarnów", + "KTA": "Tarnów powiat", + "KTT": "Tatry", + "KW": "Wadowice", + "KWA": "Wadowice powiat", + "WA": "Warszawa", + "WB": "Warszawa Bemowo", + "WBR": "Białobrzegi", + "WC": "Ciechanów", + "WCI": "Ciechanów powiat", + "WD": "Warszawa Praga Południe", + "WE": "Warszawa Praga Północ", + "WF": "Garwolin", + "WG": "Grodzisk Mazowiecki", + "WGM": "Grójec", + "WGO": "Gostynin", + "WGR": "Garwolin powiat", + "WH": "Warszawa Mokotów", + "WI": "Pruszków", + "WJ": "Józefów", + "WK": "Kozienice", + "WL": "Legionowo", + "WLI": "Lipsko", + "WLS": "Łosice", + "WM": "Mińsk Mazowiecki", + "WMA": "Maków Mazowiecki", + "WML": "Mława", + "WN": "Warszawa Białołęka", + "WND": "Nowy Dwór Mazowiecki", + "WO": "Otwock", + "WOR": "Ostrołęka", + "WOS": "Ostrów Mazowiecka", + "WOT": "Otwock powiat", + "WP": "Piaseczno", + "WPI": "Płońsk", + "WPL": "Płock", + "WPN": "Przasnysz", + "WPR": "Przysucha", + "WPU": "Pułtusk", + "WPY": "Płońsk powiat", + "WPZ": "Przasnysz powiat", + "WR": "Radom", + "WRA": "Radom powiat", + "WS": "Siedlce", + "WSC": "Sokołów Podlaski", + "WSE": "Siedlce powiat", + "WSI": "Sierpc", + "WSK": "Sochaczew", + "WSZ": "Szydłowiec", + "WT": "Warszawa Wawer", + "WU": "Warszawa Ursus", + "WV": "Ostrołęka powiat", + "WW": "Warszawa Ochota", + "WWL": "Wołomin", + "WWY": "Wyszków", + "WX": "Warszawa Ursynów", + "WY": "Warszawa Wola", + "WZ": "Żyrardów", + "WZW": "Zwoleń", + "OA": "Brzeg", + "OB": "Namysłów", + "OGL": "Głubczyce", + "OK": "Kędzierzyn-Koźle", + "OKL": "Kluczbork", + "OKR": "Krapkowice", + "OL": "Nysa", + "ONA": "Namysłów powiat", + "ONY": "Nysa powiat", + "OP": "Opole", + "OO": "Opole powiat", + "OOL": "Olesno", + "OPO": "Prudnik", + "OST": "Strzelce Opolskie", + "RB": "Brzozów", + "RBI": "Biłgoraj", + "RC": "Rzeszów Centrum", + "RD": "Dębica", + "RDE": "Dębica powiat", + "RJ": "Jarosław", + "RJA": "Jarosław powiat", + "RJS": "Jasło", + "RK": "Krosno", + "RKL": "Kolbuszowa", + "RKR": "Krosno powiat", + "RL": "Leżajsk", + "RLE": "Lesko", + "RLS": "Lubaczów", + "RLU": "Łańcut", + "RM": "Mielec", + "RMI": "Mielec powiat", + "RN": "Nisko", + "RP": "Przemyśl", + "RPR": "Przemyśl powiat", + "RPZ": "Przeworsk", + "RR": "Rzeszów", + "RRS": "Ropczyce-Sędziszów", + "RRZ": "Rzeszów powiat", + "RSA": "Sanok", + "RSN": "Sanok powiat", + "RSR": "Stalowa Wola", + "RST": "Strzyżów", + "RTA": "Tarnobrzeg", + "RZ": "Rzeszów", + "BA": "Augustów", + "BBI": "Białystok", + "BC": "Hajnówka", + "BD": "Bielsk Podlaski", + "BE": "Wysokie Mazowieckie", + "BG": "Grajewo", + "BGR": "Grajewo powiat", + "BH": "Hajnówka powiat", + "BHA": "Hajnówka", + "BI": "Białystok", + "BIA": "Białystok powiat", + "BJ": "Kolno", + "BK": "Kolno powiat", + "BKL": "Kolno", + "BL": "Łomża", + "BLM": "Łomża powiat", + "BLS": "Łomża", + "BM": "Mońki", + "BMN": "Mońki powiat", + "BO": "Sokółka", + "BP": "Zambrów", + "BPI": "Piątnica", + "BR": "Siemiatycze", + "BS": "Sokółka powiat", + "BSE": "Sejny", + "BSI": "Siemiatycze powiat", + "BSK": "Sokółka", + "BSU": "Suwałki", + "BT": "Suwałki powiat", + "BWM": "Wysokie Mazowieckie powiat", + "BZA": "Zambrów powiat", + "GA": "Gdańsk", + "GB": "Bytów", + "GBY": "Bytów powiat", + "GC": "Chojnice", + "GCH": "Chojnice powiat", + "GCZ": "Człuchów", + "GD": "Gdańsk", + "GDA": "Gdańsk powiat", + "GDY": "Gdynia", + "GI": "Kościerzyna", + "GKA": "Kartuzy", + "GKS": "Kościerzyna powiat", + "GKW": "Kwidzyn", + "GL": "Lębork", + "GLE": "Lębork powiat", + "GMB": "Malbork", + "GND": "Nowy Dwór Gdański", + "GP": "Puck", + "GPU": "Puck powiat", + "GS": "Słupsk", + "GSL": "Słupsk powiat", + "GSP": "Starogard Gdański", + "GST": "Sztum", + "GT": "Tczew", + "GTB": "Tczew powiat", + "GW": "Wejherowo", + "GWE": "Wejherowo powiat", + "SA": "Sosnowiec", + "SB": "Bielsko-Biała", + "SBB": "Bielsko-Biała powiat", + "SBE": "Będzin", + "SBI": "Bieruń-Lędziny", + "SC": "Chorzów", + "SCH": "Cieszyn", + "SCI": "Cieszyn powiat", + "SD": "Dąbrowa Górnicza", + "SF": "Racibórz", + "SG": "Gliwice", + "SGI": "Gliwice powiat", + "SH": "Chorzów", + "SI": "Siemianowice Śląskie", + "SJ": "Jastrzębie-Zdrój", + "SJZ": "Jastrzębie-Zdrój", + "SK": "Katowice", + "SKA": "Katowice powiat", + "SKL": "Kłobuck", + "SKT": "Lubliniec", + "SL": "Rybnik", + "SLU": "Lubliniec powiat", + "SM": "Mysłowice", + "SMI": "Mikołów", + "SML": "Myszków", + "SN": "Nowy Targ", + "SO": "Sosnowiec powiat", + "SP": "Piekary Śląskie", + "SPI": "Pszczyna", + "SPS": "Pszczyna powiat", + "SR": "Rybnik powiat", + "SRC": "Racibórz powiat", + "SRY": "Rybnik", + "SS": "Świętochłowice", + "ST": "Tychy", + "STA": "Tarnowskie Góry", + "STG": "Tarnowskie Góry powiat", + "SW": "Wodzisław Śląski", + "SWD": "Wodzisław Śląski powiat", + "SY": "Ruda Śląska", + "SZ": "Zabrze", + "SZA": "Zawiercie", + "SZO": "Żory", + "SZY": "Żywiec", + "TB": "Busko-Zdrój", + "TBU": "Busko-Zdrój powiat", + "TJE": "Jędrzejów", + "TK": "Kielce", + "TKA": "Kazimierza Wielka", + "TKI": "Kielce powiat", + "TKN": "Końskie", + "TKO": "Końskie powiat", + "TOS": "Ostrołęka", + "TPI": "Pińczów", + "TSA": "Sandomierz", + "TSK": "Skarżysko-Kamienna", + "TST": "Starachowice", + "TWL": "Włoszczowa", + "NBA": "Bartoszyce", + "NBR": "Braniewo", + "NDZ": "Działdowo", + "NE": "Elbląg", + "NEL": "Elbląg powiat", + "NEB": "Ełk", + "NEK": "Ełk powiat", + "NG": "Giżycko", + "NGI": "Giżycko powiat", + "NGO": "Gołdap", + "NI": "Iława", + "NKE": "Kętrzyn", + "NL": "Lidzbark Warmiński", + "NMR": "Mrągowo", + "NNI": "Nidzica", + "NO": "Olsztyn", + "NOE": "Olecko", + "NOL": "Olsztyn powiat", + "NOS": "Ostróda", + "NPI": "Pisz", + "NSZ": "Szczytno", + "NW": "Węgorzewo", + "PCD": "Czarnków-Trzcianka", + "PCH": "Chodzież", + "PGN": "Gniezno", + "PGO": "Gostyń", + "PGR": "Grodzisk Wielkopolski", + "PIA": "Piła", + "PJ": "Jarocin", + "PJA": "Jarocin powiat", + "PK": "Kępno", + "PKA": "Kalisz", + "PKL": "Kalisz powiat", + "PKN": "Koło", + "PKO": "Konin", + "PKS": "Kościan", + "PL": "Leszno", + "PLE": "Leszno powiat", + "PMI": "Międzychód", + "PNT": "Nowy Tomyśl", + "PO": "Poznań", + "POB": "Oborniki", + "POL": "Ostrów Wielkopolski", + "POP": "Opole", + "POS": "Ostrzeszów", + "POT": "Ostrów Wielkopolski powiat", + "PP": "Pleszew", + "PPI": "Piła powiat", + "PPL": "Pleszew powiat", + "PRA": "Poznań powiat", + "PRS": "Rawicz", + "PSE": "Śrem", + "PSL": "Słupca", + "PSR": "Środa Wielkopolska", + "PSZ": "Szamotuły", + "PT": "Turek", + "PTU": "Turek powiat", + "PW": "Wągrowiec", + "PWA": "Wągrowiec powiat", + "PWL": "Wolsztyn", + "PWR": "Września", + "PZ": "Poznań", + "PZL": "Złotów", + "ZBI": "Białogard", + "ZCH": "Choszczno", + "ZG": "Gryfice", + "ZGR": "Gryfino", + "ZI": "Stargard", + "ZK": "Kołobrzeg", + "ZKA": "Kamień Pomorski", + "ZKL": "Kołobrzeg powiat", + "ZKO": "Koszalin", + "ZKS": "Koszalin powiat", + "ZL": "Łobez", + "ZM": "Myślibórz", + "ZPL": "Pyrzyce", + "ZPO": "Police", + "ZS": "Szczecin", + "ZSL": "Sławno", + "ZST": "Stargard powiat", + "ZSW": "Świnoujście", + "ZSZ": "Szczecinek", + "ZW": "Wałcz", + "ZZ": "Szczecin powiat", +} diff --git a/python_pkg/anki_decks/polish_license_plates/polish_license_plates_anki.py b/python_pkg/anki_decks/polish_license_plates/polish_license_plates_anki.py new file mode 100644 index 0000000..60752f0 --- /dev/null +++ b/python_pkg/anki_decks/polish_license_plates/polish_license_plates_anki.py @@ -0,0 +1,244 @@ +"""Anki flashcard generator for Polish car license plates. + +Generates Anki-compatible flashcard decks with bidirectional cards for Polish +vehicle registration plate codes and their corresponding locations. + +Creates two types of cards: +1. Code → Location (e.g., WY → Warszawa Wola) +2. Location → Code (e.g., Warszawa Wola → WY) + +Usage: + # Generate Anki cards for all Polish license plates + python -m python_pkg.anki_decks.polish_license_plates.polish_license_plates_anki + + # Specify custom output file + python -m python_pkg.anki_decks.polish_license_plates.polish_license_plates_anki \ + --output plates.apkg + +Output: + Creates a self-contained .apkg file that can be directly imported into Anki. +""" + +from __future__ import annotations + +import argparse +import hashlib +from pathlib import Path +import random +import sys +from typing import TYPE_CHECKING + +import genanki + +from python_pkg.anki_decks.polish_license_plates.license_plate_data import ( + LICENSE_PLATE_CODES, +) + +if TYPE_CHECKING: + from collections.abc import Sequence + + +def generate_anki_package( + deck_name: str = "Polish License Plates", +) -> genanki.Package: + """Generate Anki package (.apkg) for Polish license plates. + + Creates two cards for each license plate code: + 1. Code → Location + 2. Location → Code + + Args: + deck_name: Name for the Anki deck. + + Returns: + genanki.Package object ready to be written to file. + """ + # Create unique model ID based on deck name + model_id_hash = hashlib.md5( + f"polish_license_plates_{deck_name}".encode(), + usedforsecurity=False, + ) + model_id = int(model_id_hash.hexdigest()[:8], 16) + + # Define the note model with centered styling and bidirectional templates + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 28px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.question { + display: flex; + justify-content: center; + align-items: center; + min-height: 60vh; + font-size: 48px; + font-weight: bold; + color: #2C3E50; +} +.card.night_mode .question { + color: #ECF0F1; +} +.answer { + font-size: 36px; + font-weight: bold; + margin-top: 20px; + color: #27AE60; +} +.card.night_mode .answer { + color: #2ECC71; +} +.plate-code { + font-family: 'Courier New', monospace; + background-color: #FFD700; + color: #000; + padding: 15px 30px; + border: 3px solid #000; + border-radius: 8px; + display: inline-block; + letter-spacing: 5px; +} +.card.night_mode .plate-code { + background-color: #FFA500; +} +""" + + my_model = genanki.Model( + model_id, + "Polish License Plate Model", + fields=[ + {"name": "Code"}, + {"name": "Location"}, + ], + templates=[ + { + "name": "Code → Location", + "qfmt": '
' + '{{Code}}' + "
", + "afmt": '
' + '{{Code}}' + "
" + '
' + '
{{Location}}
', + }, + { + "name": "Location → Code", + "qfmt": '
{{Location}}
', + "afmt": '
{{Location}}
' + '
' + '
' + '{{Code}}' + "
", + }, + ], + css=card_css, + ) + + # Create unique deck ID + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + + # Create the deck + my_deck = genanki.Deck(deck_id, deck_name) + + # Generate notes for each license plate code + for code, location in sorted(LICENSE_PLATE_CODES.items()): + note = genanki.Note( + model=my_model, + fields=[code, location], + tags=["geography", "poland", "license-plates", "transportation"], + ) + my_deck.add_note(note) + + # Create package + return genanki.Package(my_deck) + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point. + + Args: + argv: Command line arguments. + + Returns: + Exit code. + """ + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish license plates.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_license_plates.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish License Plates", + help="Name for the Anki deck (default: 'Polish License Plates')", + ) + + args = parser.parse_args(argv) + + # Determine output path + output_path = ( + Path(args.output) if args.output else Path("polish_license_plates.apkg") + ) + + try: + num_codes = len(LICENSE_PLATE_CODES) + num_cards = num_codes * 2 # Two cards per code (bidirectional) + + sys.stdout.write( + f"Generating flashcards for {num_codes} Polish license plate codes...\n" + ) + sys.stdout.write( + "Each code will have 2 cards: Code → Location and Location → Code\n" + ) + + # Generate the package + package = generate_anki_package(args.deck_name) + + # Write to file + package.write_to_file(str(output_path)) + + sys.stdout.write("\n") + sys.stdout.write("=" * 70 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 70 + "\n") + sys.stdout.write(f"License plate codes: {num_codes}\n") + sys.stdout.write(f"Total flashcards: {num_cards} (bidirectional)\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + sys.stdout.write("\n") + sys.stdout.write("Card types:\n") + sys.stdout.write(" 1. Code → Location (e.g., WY → Warszawa Wola)\n") + sys.stdout.write(" 2. Location → Code (e.g., Warszawa Wola → WY)\n") + sys.stdout.write("\n") + sys.stdout.write("To import into Anki:\n") + sys.stdout.write(" 1. Open Anki\n") + sys.stdout.write(" 2. File → Import\n") + sys.stdout.write(f" 3. Select: {output_path.absolute()}\n") + sys.stdout.write(" 4. Click Import\n") + sys.stdout.write("\n") + sys.stdout.write("You can now learn Polish license plates both ways!\n") + except (OSError, ValueError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_license_plates/tests/__init__.py b/python_pkg/anki_decks/polish_license_plates/tests/__init__.py new file mode 100644 index 0000000..4f70ec1 --- /dev/null +++ b/python_pkg/anki_decks/polish_license_plates/tests/__init__.py @@ -0,0 +1 @@ +"""Tests init file.""" diff --git a/python_pkg/anki_decks/polish_license_plates/tests/test_polish_license_plates_anki.py b/python_pkg/anki_decks/polish_license_plates/tests/test_polish_license_plates_anki.py new file mode 100644 index 0000000..0742325 --- /dev/null +++ b/python_pkg/anki_decks/polish_license_plates/tests/test_polish_license_plates_anki.py @@ -0,0 +1,231 @@ +"""Tests for the Polish license plates Anki generator.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +try: + from python_pkg.anki_decks.polish_license_plates.license_plate_data import ( + LICENSE_PLATE_CODES, + ) + from python_pkg.anki_decks.polish_license_plates.polish_license_plates_anki import ( + generate_anki_package, + main, + ) +except ImportError: + import sys + + sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent)) + from python_pkg.anki_decks.polish_license_plates.license_plate_data import ( + LICENSE_PLATE_CODES, + ) + from python_pkg.anki_decks.polish_license_plates.polish_license_plates_anki import ( + generate_anki_package, + main, + ) + + +class TestLicensePlateData: + """Tests for license plate data.""" + + def test_has_codes(self) -> None: + """Test that we have license plate codes.""" + assert len(LICENSE_PLATE_CODES) > 0 + + def test_all_codes_are_uppercase(self) -> None: + """Test that all codes are uppercase strings.""" + for code in LICENSE_PLATE_CODES: + assert isinstance(code, str) + assert code.isupper() + assert len(code) >= 2 + + def test_all_locations_are_strings(self) -> None: + """Test that all locations are non-empty strings.""" + for location in LICENSE_PLATE_CODES.values(): + assert isinstance(location, str) + assert len(location) > 0 + + def test_no_duplicate_codes(self) -> None: + """Test that all codes are unique.""" + codes = list(LICENSE_PLATE_CODES.keys()) + assert len(codes) == len(set(codes)) + + def test_warsaw_codes_present(self) -> None: + """Test that Warsaw codes are in the database.""" + warsaw_codes = [ + "WA", + "WB", + "WC", + "WD", + "WE", + "WF", + "WG", + "WH", + "WI", + "WJ", + "WK", + "WL", + "WM", + "WN", + "WO", + "WP", + "WR", + "WS", + "WT", + "WU", + "WW", + "WX", + "WY", + "WZ", + ] + for code in warsaw_codes: + assert code in LICENSE_PLATE_CODES + + def test_major_cities_present(self) -> None: + """Test that major Polish cities have codes.""" + major_cities = { + "WA": "Warszawa", + "KR": "Kraków", + "GD": "Gdańsk", + "PO": "Poznań", + "WR": "Radom", + "BI": "Białystok", + } + for code, city_part in major_cities.items(): + assert code in LICENSE_PLATE_CODES + assert city_part.lower() in LICENSE_PLATE_CODES[code].lower() + + def test_voivodeship_prefixes_present(self) -> None: + """Test that all 16 voivodeship prefixes are represented.""" + voivodeship_prefixes = { + "B", + "C", + "D", + "E", + "F", + "G", + "K", + "L", + "N", + "O", + "P", + "R", + "S", + "T", + "W", + "Z", + } + found_prefixes = {code[0] for code in LICENSE_PLATE_CODES} + assert voivodeship_prefixes.issubset(found_prefixes) + + +class TestGenerateAnkiPackage: + """Tests for generating Anki package.""" + + def test_generates_package(self) -> None: + """Test that output is a genanki Package.""" + package = generate_anki_package("Test Deck") + assert package is not None + assert len(package.decks) == 1 + + def test_generates_notes_for_all_codes(self) -> None: + """Test that package contains notes for all license plate codes.""" + package = generate_anki_package() + deck = package.decks[0] + # Each code generates one note with two card templates + assert len(deck.notes) == len(LICENSE_PLATE_CODES) + + def test_custom_deck_name(self) -> None: + """Test that custom deck name is used.""" + package = generate_anki_package("Custom Deck") + deck = package.decks[0] + assert deck.name == "Custom Deck" + + def test_notes_have_correct_fields(self) -> None: + """Test that notes have Code and Location fields.""" + package = generate_anki_package() + deck = package.decks[0] + note = deck.notes[0] + # Note should have 2 fields: Code and Location + assert len(note.fields) == 2 + # Fields should be non-empty strings + assert len(note.fields[0]) > 0 + assert len(note.fields[1]) > 0 + + def test_notes_have_tags(self) -> None: + """Test that notes have appropriate tags.""" + package = generate_anki_package() + deck = package.decks[0] + note = deck.notes[0] + assert "geography" in note.tags + assert "poland" in note.tags + assert "license-plates" in note.tags + + def test_model_has_bidirectional_templates(self) -> None: + """Test that the model has two card templates (bidirectional).""" + package = generate_anki_package() + deck = package.decks[0] + model = deck.notes[0].model + # Should have 2 templates: Code → Location and Location → Code + assert len(model.templates) == 2 + + +class TestMain: + """Tests for the main CLI function.""" + + def test_creates_output_file(self, tmp_path: Path) -> None: + """Test that main creates the output file.""" + output_file = tmp_path / "test_output.apkg" + + result = main( + [ + "--output", + str(output_file), + ] + ) + + assert result == 0 + assert output_file.exists() + + def test_custom_deck_name(self, tmp_path: Path) -> None: + """Test that custom deck name is used.""" + output_file = tmp_path / "test_output.apkg" + + result = main( + [ + "--output", + str(output_file), + "--deck-name", + "Custom Deck", + ] + ) + + assert result == 0 + assert output_file.exists() + + def test_default_output_path(self) -> None: + """Test that default output path is used when not specified.""" + # Clean up any existing file + default_path = Path("polish_license_plates.apkg") + if default_path.exists(): + default_path.unlink() + + result = main([]) + + assert result == 0 + assert default_path.exists() + + # Clean up + default_path.unlink() + + def test_help_flag(self) -> None: + """Test that --help works.""" + with pytest.raises(SystemExit) as exc_info: + main(["--help"]) + assert exc_info.value.code == 0 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/python_pkg/anki_decks/polish_mountain_peaks/__init__.py b/python_pkg/anki_decks/polish_mountain_peaks/__init__.py new file mode 100644 index 0000000..ec74a73 --- /dev/null +++ b/python_pkg/anki_decks/polish_mountain_peaks/__init__.py @@ -0,0 +1 @@ +"""Polish mountain peaks Anki deck generator.""" diff --git a/python_pkg/anki_decks/polish_mountain_peaks/polish_mountain_peaks_anki.py b/python_pkg/anki_decks/polish_mountain_peaks/polish_mountain_peaks_anki.py new file mode 100644 index 0000000..c466cf0 --- /dev/null +++ b/python_pkg/anki_decks/polish_mountain_peaks/polish_mountain_peaks_anki.py @@ -0,0 +1,378 @@ +"""Anki flashcard generator for Polish mountain peaks. + +Generates Anki-compatible flashcard decks with ZOOMED maps showing mountain peaks +highlighted on a regional map for better visibility. +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_mountain_peaks + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +MARKER_COLOR = "#E74C3C" # Red marker for peaks +ZOOM_PADDING_DEG = 0.5 # Degrees of padding around peak for zoomed view + + +def create_peak_map( + peak_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + *, + zoom: bool = True, +) -> Figure: + """Create a map showing Poland with one peak highlighted. + + Args: + peak_gdf: GeoDataFrame with the peak point. + poland_boundary: GeoDataFrame with Poland boundary. + zoom: If True, zoom to peak area; if False, show entire Poland. + """ + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Plot the peak as a marker + peak_gdf.plot( + ax=ax, + color=MARKER_COLOR, + markersize=400 if zoom else 200, + marker="^", # Triangle for mountain + edgecolor="#1A1A1A", + linewidth=2, + zorder=5, + ) + + if zoom: + # Zoom to peak area with padding + geom = peak_gdf.iloc[0].geometry + peak_x, peak_y = geom.x, geom.y + ax.set_xlim(peak_x - ZOOM_PADDING_DEG, peak_x + ZOOM_PADDING_DEG) + ax.set_ylim(peak_y - ZOOM_PADDING_DEG, peak_y + ZOOM_PADDING_DEG) + else: + # Set bounds to Poland + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_peak_image_bytes( + peak_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + *, + zoom: bool = True, +) -> bytes: + """Generate a peak map image as bytes.""" + fig = create_peak_map(peak_gdf, poland_boundary, zoom=zoom) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None +_mp_zoom: bool = True + + +def _init_worker(poland_geojson: str, zoom_mode: str) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary, _mp_zoom # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + _mp_zoom = zoom_mode == "zoom" + + +def _render_single_peak(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single peak image (worker function). + + Args: + args: Tuple of (peak_name, peak_geojson_str). + + Returns: + Tuple of (peak_name, image_bytes). + """ + peak_name, peak_geojson = args + peak_gdf = gpd.read_file(peak_geojson) + + assert _mp_poland_boundary is not None # noqa: S101 + + image_data = generate_peak_image_bytes(peak_gdf, _mp_poland_boundary, zoom=_mp_zoom) + return peak_name, image_data + + +def generate_anki_package( + peaks: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish Mountain Peaks", + *, + zoom: bool = True, +) -> genanki.Package: + """Generate Anki package for Polish mountain peaks.""" + model_id_hash = hashlib.md5( # noqa: S324 + f"polish_mountain_peaks_{deck_name}".encode() + ) + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.info-text { + font-size: 18px; + color: #7F8C8D; + margin-top: 10px; +} +.card.night_mode .info-text { + color: #BDC3C7; +} +""" + + my_model = genanki.Model( + model_id, + "Polish Mountain Peak Model", + fields=[ + {"name": "PeakMap"}, + {"name": "PeakName"}, + {"name": "Elevation"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{PeakMap}}
', + "afmt": '
{{PeakMap}}
' + '
' + '
{{PeakName}}
' + '
{{Elevation}} m n.p.m.
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Prepare data for parallel processing + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (peak_name, peak_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in peaks.iterrows(): + peak_gdf = gpd.GeoDataFrame([row], crs=peaks.crs) + peak_geojson = peak_gdf.to_json() + work_items.append((row["name"], peak_geojson)) + + # Use multiprocessing for parallel rendering + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path, "zoom" if zoom else "no-zoom"), + ) as pool: + for i, (peak_name, image_data) in enumerate( + pool.imap_unordered(_render_single_peak, work_items) + ): + results[peak_name] = image_data + if (i + 1) % 50 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in peaks.iterrows(): + peak_name = row["name"] + elevation = int(row["elevation"]) + image_data = results[peak_name] + filename = f"peak_{peak_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', peak_name, str(elevation)], + tags=["geography", "poland", "mountains", "peaks"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish mountain peaks.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_mountain_peaks.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish Mountain Peaks", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + parser.add_argument( + "--no-zoom", + action="store_true", + help="Disable zoom (show entire Poland instead of zoomed region)", + ) + parser.add_argument( + "--limit", + "-l", + type=int, + default=None, + help="Limit number of peaks (for testing)", + ) + + args = parser.parse_args(argv) + output_path = ( + Path(args.output) if args.output else Path("polish_mountain_peaks.apkg") + ) + zoom = not args.no_zoom + + try: + sys.stdout.write("Loading mountain peaks data...\n") + peaks = get_polish_mountain_peaks() + poland_boundary = get_poland_boundary() + + if args.limit: + peaks = peaks.head(args.limit) + sys.stdout.write(f"Limiting to {args.limit} peaks.\n") + + num_peaks = len(peaks) + + sys.stdout.write(f"Found {num_peaks} mountain peaks.\n") + sys.stdout.write(f"Zoom mode: {'enabled' if zoom else 'disabled'}\n") + sys.stdout.write("Generating flashcards...\n") + + package = generate_anki_package( + peaks, poland_boundary, args.deck_name, zoom=zoom + ) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_peaks = list(peaks.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_peaks)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_peaks: + peak_name = row["name"] + peak_gdf = gpd.GeoDataFrame([row], crs=peaks.crs) + image_data = generate_peak_image_bytes( + peak_gdf, poland_boundary, zoom=zoom + ) + safe_name = peak_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Mountain peaks: {num_peaks}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_mountain_peaks/run.sh b/python_pkg/anki_decks/polish_mountain_peaks/run.sh new file mode 100755 index 0000000..cc431fd --- /dev/null +++ b/python_pkg/anki_decks/polish_mountain_peaks/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run the Polish mountain peaks Anki generator + +cd "$(dirname "$0")" || exit + +python polish_mountain_peaks_anki.py --preview preview_images --preview-count 5 "$@" diff --git a/python_pkg/anki_decks/polish_mountain_ranges/__init__.py b/python_pkg/anki_decks/polish_mountain_ranges/__init__.py new file mode 100644 index 0000000..1c52609 --- /dev/null +++ b/python_pkg/anki_decks/polish_mountain_ranges/__init__.py @@ -0,0 +1 @@ +"""Polish mountain ranges Anki generator.""" diff --git a/python_pkg/anki_decks/polish_mountain_ranges/polish_mountain_ranges_anki.py b/python_pkg/anki_decks/polish_mountain_ranges/polish_mountain_ranges_anki.py new file mode 100644 index 0000000..68d71e5 --- /dev/null +++ b/python_pkg/anki_decks/polish_mountain_ranges/polish_mountain_ranges_anki.py @@ -0,0 +1,332 @@ +"""Anki flashcard generator for Polish mountain ranges. + +Generates Anki-compatible flashcard decks with maps showing mountain ranges +highlighted on a Poland map. +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_mountain_ranges + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +RANGE_COLOR = "#7B5A31" # Brown for mountain ranges + + +def create_range_map( + range_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Poland with one mountain range highlighted.""" + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Clip mountain range to Poland boundary + clipped_gdf = range_gdf.copy() + clipped_gdf["geometry"] = range_gdf.geometry.intersection( + poland_boundary.union_all() + ) + + # Plot the mountain range (clipped to Poland) + clipped_gdf.plot(ax=ax, color=RANGE_COLOR, alpha=0.9) + clipped_gdf.boundary.plot(ax=ax, color="#1A1A1A", linewidth=1.5) + + # Set bounds to Poland + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_range_image_bytes( + range_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> bytes: + """Generate a range map image as bytes.""" + fig = create_range_map(range_gdf, poland_boundary) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None + + +def _init_worker(poland_geojson: str) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + + +def _render_single_range(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single range image (worker function). + + Args: + args: Tuple of (range_name, range_geojson_str). + + Returns: + Tuple of (range_name, image_bytes). + """ + range_name, range_geojson = args + range_gdf = gpd.read_file(range_geojson) + + assert _mp_poland_boundary is not None # noqa: S101 + + image_data = generate_range_image_bytes(range_gdf, _mp_poland_boundary) + return range_name, image_data + + +def generate_anki_package( + ranges: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish Mountain Ranges", +) -> genanki.Package: + """Generate Anki package for Polish mountain ranges.""" + model_id_hash = hashlib.md5( # noqa: S324 + f"polish_mountain_ranges_{deck_name}".encode() + ) + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.info-text { + font-size: 18px; + color: #7F8C8D; + margin-top: 10px; +} +.card.night_mode .info-text { + color: #BDC3C7; +} +""" + + my_model = genanki.Model( + model_id, + "Polish Mountain Range Model", + fields=[ + {"name": "RangeMap"}, + {"name": "RangeName"}, + {"name": "Area"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{RangeMap}}
', + "afmt": '
{{RangeMap}}
' + '
' + '
{{RangeName}}
' + '
{{Area}} km²
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Prepare data for parallel processing + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (range_name, range_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in ranges.iterrows(): + range_gdf = gpd.GeoDataFrame([row], crs=ranges.crs) + range_geojson = range_gdf.to_json() + work_items.append((row["name"], range_geojson)) + + # Use multiprocessing for parallel rendering + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path,), + ) as pool: + for i, (range_name, image_data) in enumerate( + pool.imap_unordered(_render_single_range, work_items) + ): + results[range_name] = image_data + if (i + 1) % 10 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in ranges.iterrows(): + range_name = row["name"] + area_km2 = round(row["area_km2"], 1) if "area_km2" in row else 0 + image_data = results[range_name] + filename = f"range_{range_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', range_name, str(area_km2)], + tags=["geography", "poland", "mountain-ranges", "mountains"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish mountain ranges.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_mountain_ranges.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish Mountain Ranges", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = ( + Path(args.output) if args.output else Path("polish_mountain_ranges.apkg") + ) + + try: + sys.stdout.write("Loading mountain ranges data...\n") + ranges = get_polish_mountain_ranges() + poland_boundary = get_poland_boundary() + num_ranges = len(ranges) + + sys.stdout.write(f"Found {num_ranges} mountain ranges.\n") + sys.stdout.write("Generating flashcards...\n") + + package = generate_anki_package(ranges, poland_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_ranges = list(ranges.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_ranges)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_ranges: + range_name = row["name"] + range_gdf = gpd.GeoDataFrame([row], crs=ranges.crs) + image_data = generate_range_image_bytes(range_gdf, poland_boundary) + safe_name = range_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Mountain ranges: {num_ranges}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_mountain_ranges/run.sh b/python_pkg/anki_decks/polish_mountain_ranges/run.sh new file mode 100755 index 0000000..1ff10c0 --- /dev/null +++ b/python_pkg/anki_decks/polish_mountain_ranges/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run the Polish mountain ranges Anki generator + +cd "$(dirname "$0")" || exit + +python polish_mountain_ranges_anki.py --preview preview_images --preview-count 5 "$@" diff --git a/python_pkg/anki_decks/polish_national_parks/__init__.py b/python_pkg/anki_decks/polish_national_parks/__init__.py new file mode 100644 index 0000000..8094984 --- /dev/null +++ b/python_pkg/anki_decks/polish_national_parks/__init__.py @@ -0,0 +1 @@ +"""Polish national parks Anki deck generator.""" diff --git a/python_pkg/anki_decks/polish_national_parks/polish_national_parks_anki.py b/python_pkg/anki_decks/polish_national_parks/polish_national_parks_anki.py new file mode 100644 index 0000000..a7cd480 --- /dev/null +++ b/python_pkg/anki_decks/polish_national_parks/polish_national_parks_anki.py @@ -0,0 +1,348 @@ +"""Anki flashcard generator for Polish national parks. + +Generates Anki-compatible flashcard decks with maps showing national parks +highlighted on a Poland map. +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_national_parks + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +PARK_COLOR = "#2ECC71" # Green for national parks + +# Threshold for "small" parks that need an icon (in km²) +SMALL_PARK_THRESHOLD_KM2 = 100 + + +def create_park_map( + park_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Poland with one national park highlighted. + + For small parks, also shows a tree marker at the centroid for visibility. + """ + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Plot the national park + park_gdf.plot(ax=ax, color=PARK_COLOR, alpha=0.9) + park_gdf.boundary.plot(ax=ax, color="#1A1A1A", linewidth=1.5) + + # For small parks, add a tree marker at the centroid + area_km2 = park_gdf.iloc[0].get("area_km2", 0) + if area_km2 < SMALL_PARK_THRESHOLD_KM2: + centroid = park_gdf.iloc[0].geometry.centroid + # Use a tree-like marker (triangle pointing up) + ax.scatter( + [centroid.x], + [centroid.y], + s=600, + c="#006400", + marker="^", + edgecolor="#1A1A1A", + linewidth=2, + zorder=10, + ) + + # Set bounds to Poland + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_park_image_bytes( + park_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> bytes: + """Generate a park map image as bytes.""" + fig = create_park_map(park_gdf, poland_boundary) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None + + +def _init_worker(poland_geojson: str) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + + +def _render_single_park(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single park image (worker function). + + Args: + args: Tuple of (park_name, park_geojson_str). + + Returns: + Tuple of (park_name, image_bytes). + """ + park_name, park_geojson = args + park_gdf = gpd.read_file(park_geojson) + + assert _mp_poland_boundary is not None # noqa: S101 + + image_data = generate_park_image_bytes(park_gdf, _mp_poland_boundary) + return park_name, image_data + + +def generate_anki_package( + parks: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish National Parks", +) -> genanki.Package: + """Generate Anki package for Polish national parks.""" + model_id_hash = hashlib.md5( # noqa: S324 + f"polish_national_parks_{deck_name}".encode() + ) + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.info-text { + font-size: 18px; + color: #7F8C8D; + margin-top: 10px; +} +.card.night_mode .info-text { + color: #BDC3C7; +} +""" + + my_model = genanki.Model( + model_id, + "Polish National Park Model", + fields=[ + {"name": "ParkMap"}, + {"name": "ParkName"}, + {"name": "Area"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{ParkMap}}
', + "afmt": '
{{ParkMap}}
' + '
' + '
{{ParkName}}
' + '
{{Area}} km²
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Prepare data for parallel processing + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (park_name, park_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in parks.iterrows(): + park_gdf = gpd.GeoDataFrame([row], crs=parks.crs) + park_geojson = park_gdf.to_json() + work_items.append((row["name"], park_geojson)) + + # Use multiprocessing for parallel rendering + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path,), + ) as pool: + for i, (park_name, image_data) in enumerate( + pool.imap_unordered(_render_single_park, work_items) + ): + results[park_name] = image_data + if (i + 1) % 10 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in parks.iterrows(): + park_name = row["name"] + area_km2 = round(row["area_km2"], 1) if "area_km2" in row else 0 + image_data = results[park_name] + filename = f"park_{park_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', park_name, str(area_km2)], + tags=["geography", "poland", "national-parks", "nature"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish national parks.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_national_parks.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish National Parks", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = ( + Path(args.output) if args.output else Path("polish_national_parks.apkg") + ) + + try: + sys.stdout.write("Loading national parks data...\n") + parks = get_polish_national_parks() + poland_boundary = get_poland_boundary() + num_parks = len(parks) + + sys.stdout.write(f"Found {num_parks} national parks.\n") + sys.stdout.write("Generating flashcards...\n") + + package = generate_anki_package(parks, poland_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_parks = list(parks.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_parks)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_parks: + park_name = row["name"] + park_gdf = gpd.GeoDataFrame([row], crs=parks.crs) + image_data = generate_park_image_bytes(park_gdf, poland_boundary) + safe_name = park_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"National parks: {num_parks}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_national_parks/run.sh b/python_pkg/anki_decks/polish_national_parks/run.sh new file mode 100755 index 0000000..c31f77b --- /dev/null +++ b/python_pkg/anki_decks/polish_national_parks/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run the Polish national parks Anki generator + +cd "$(dirname "$0")" || exit + +python polish_national_parks_anki.py --preview preview_images --preview-count 5 "$@" diff --git a/python_pkg/anki_decks/polish_nature_reserves/__init__.py b/python_pkg/anki_decks/polish_nature_reserves/__init__.py new file mode 100644 index 0000000..24de2f5 --- /dev/null +++ b/python_pkg/anki_decks/polish_nature_reserves/__init__.py @@ -0,0 +1 @@ +"""Polish nature reserves Anki generator.""" diff --git a/python_pkg/anki_decks/polish_nature_reserves/polish_nature_reserves_anki.py b/python_pkg/anki_decks/polish_nature_reserves/polish_nature_reserves_anki.py new file mode 100644 index 0000000..6ce086e --- /dev/null +++ b/python_pkg/anki_decks/polish_nature_reserves/polish_nature_reserves_anki.py @@ -0,0 +1,345 @@ +"""Anki flashcard generator for Polish nature reserves. + +Generates Anki-compatible flashcard decks with maps showing nature reserves +highlighted on a Poland map. Optimized for large datasets (~1500 reserves). +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_nature_reserves + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +RESERVE_COLOR = "#16A085" # Teal for nature reserves + + +def create_reserve_map( + reserve_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Poland with one nature reserve highlighted.""" + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Plot the nature reserve + reserve_gdf.plot(ax=ax, color=RESERVE_COLOR, alpha=0.9) + reserve_gdf.boundary.plot(ax=ax, color="#1A1A1A", linewidth=3) + + # Set bounds to Poland + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_reserve_image_bytes( + reserve_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> bytes: + """Generate a reserve map image as bytes.""" + fig = create_reserve_map(reserve_gdf, poland_boundary) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None + + +def _init_worker(poland_geojson: str) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + + +def _render_single_reserve(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single reserve image (worker function). + + Args: + args: Tuple of (reserve_name, reserve_geojson_str). + + Returns: + Tuple of (reserve_name, image_bytes). + """ + reserve_name, reserve_geojson = args + reserve_gdf = gpd.read_file(reserve_geojson) + + assert _mp_poland_boundary is not None # noqa: S101 + + image_data = generate_reserve_image_bytes(reserve_gdf, _mp_poland_boundary) + return reserve_name, image_data + + +def generate_anki_package( + reserves: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish Nature Reserves", +) -> genanki.Package: + """Generate Anki package for Polish nature reserves.""" + model_id_hash = hashlib.md5( # noqa: S324 + f"polish_nature_reserves_{deck_name}".encode() + ) + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 28px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.info-text { + font-size: 18px; + color: #7F8C8D; + margin-top: 10px; +} +.card.night_mode .info-text { + color: #BDC3C7; +} +""" + + my_model = genanki.Model( + model_id, + "Polish Nature Reserve Model", + fields=[ + {"name": "ReserveMap"}, + {"name": "ReserveName"}, + {"name": "Area"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{ReserveMap}}
', + "afmt": '
{{ReserveMap}}
' + '
' + '
{{ReserveName}}
' + '
{{Area}} km²
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Prepare data for parallel processing + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (reserve_name, reserve_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in reserves.iterrows(): + reserve_gdf = gpd.GeoDataFrame([row], crs=reserves.crs) + reserve_geojson = reserve_gdf.to_json() + work_items.append((row["name"], reserve_geojson)) + + # Use multiprocessing for parallel rendering (more workers for large datasets) + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + sys.stdout.write("(This may take a while due to the large number of reserves)\n") + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path,), + ) as pool: + for i, (reserve_name, image_data) in enumerate( + pool.imap_unordered(_render_single_reserve, work_items) + ): + results[reserve_name] = image_data + if (i + 1) % 100 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + sys.stdout.write(f" Rendered {len(work_items)}/{len(work_items)}.\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in reserves.iterrows(): + reserve_name = row["name"] + area_km2 = round(row["area_km2"], 2) if "area_km2" in row else 0 + image_data = results[reserve_name] + # Use hash for unique filename since names may have special chars + name_hash = hashlib.md5(reserve_name.encode()).hexdigest()[:8] # noqa: S324 + safe_name = reserve_name.replace(" ", "_").replace("/", "_")[:30] + filename = f"reserve_{safe_name}_{name_hash}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', reserve_name, str(area_km2)], + tags=["geography", "poland", "nature-reserves", "protected-areas"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish nature reserves.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_nature_reserves.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish Nature Reserves", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + parser.add_argument( + "--limit", + "-l", + type=int, + default=None, + help="Limit number of reserves (for testing, default: all)", + ) + + args = parser.parse_args(argv) + output_path = ( + Path(args.output) if args.output else Path("polish_nature_reserves.apkg") + ) + + try: + sys.stdout.write("Loading nature reserves data...\n") + reserves = get_polish_nature_reserves() + poland_boundary = get_poland_boundary() + + # Apply limit if specified + if args.limit: + reserves = reserves.head(args.limit) + sys.stdout.write(f"Limiting to {args.limit} reserves.\n") + + num_reserves = len(reserves) + + sys.stdout.write(f"Found {num_reserves} nature reserves.\n") + sys.stdout.write("Generating flashcards...\n") + + package = generate_anki_package(reserves, poland_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_reserves = list(reserves.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_reserves)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_reserves: + reserve_name = row["name"] + reserve_gdf = gpd.GeoDataFrame([row], crs=reserves.crs) + image_data = generate_reserve_image_bytes(reserve_gdf, poland_boundary) + safe_name = reserve_name.replace(" ", "_").replace("/", "_")[:30] + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Nature reserves: {num_reserves}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_nature_reserves/run.sh b/python_pkg/anki_decks/polish_nature_reserves/run.sh new file mode 100755 index 0000000..de2185b --- /dev/null +++ b/python_pkg/anki_decks/polish_nature_reserves/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Run the Polish nature reserves Anki generator + +cd "$(dirname "$0")" || exit + +# Default runs all reserves - use --limit for testing +python polish_nature_reserves_anki.py --preview preview_images --preview-count 5 "$@" diff --git a/python_pkg/anki_decks/polish_powiaty/__init__.py b/python_pkg/anki_decks/polish_powiaty/__init__.py new file mode 100644 index 0000000..a01be4f --- /dev/null +++ b/python_pkg/anki_decks/polish_powiaty/__init__.py @@ -0,0 +1 @@ +"""Polish powiaty (counties) Anki flashcard generator.""" diff --git a/python_pkg/anki_decks/polish_powiaty/polish_powiaty_anki.py b/python_pkg/anki_decks/polish_powiaty/polish_powiaty_anki.py new file mode 100755 index 0000000..1080fda --- /dev/null +++ b/python_pkg/anki_decks/polish_powiaty/polish_powiaty_anki.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +"""Anki flashcard generator for Polish powiaty (counties). + +Generates Anki-compatible flashcard decks with maps showing individual +Polish counties highlighted on a country map. +""" + +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 + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_powiaty + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +# 400 distinct colors for powiaty (cycling through) +POWIAT_COLORS = [ + "#E74C3C", + "#3498DB", + "#2ECC71", + "#9B59B6", + "#F39C12", + "#1ABC9C", + "#E91E63", + "#00BCD4", + "#8BC34A", + "#FF5722", + "#673AB7", + "#FFEB3B", + "#795548", + "#607D8B", + "#CDDC39", + "#FF9800", + "#4CAF50", + "#03A9F4", + "#F44336", + "#009688", + "#3F51B5", + "#FFC107", + "#9E9E9E", + "#00E676", + "#FF4081", + "#448AFF", + "#69F0AE", + "#FFD740", + "#40C4FF", + "#B388FF", + "#EA80FC", + "#82B1FF", + "#A7FFEB", + "#FFFF8D", + "#FF80AB", + "#536DFE", + "#64FFDA", + "#FFE57F", + "#80D8FF", + "#B9F6CA", + "#CF6679", + "#BB86FC", + "#03DAC6", + "#018786", + "#6200EE", + "#3700B3", + "#B00020", + "#FF0266", + "#C51162", + "#AA00FF", +] + + +def create_powiat_map( + powiat_name: str, + powiat_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + all_powiaty: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Poland with one powiat highlighted.""" + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Assign color based on sorted names + sorted_names = sorted(all_powiaty["nazwa"].tolist()) + color_idx = sorted_names.index(powiat_name) % len(POWIAT_COLORS) + fill_color = POWIAT_COLORS[color_idx] + + # Plot the highlighted powiat + powiat_gdf.plot(ax=ax, color=fill_color, alpha=0.9) + powiat_gdf.boundary.plot(ax=ax, color="#1A1A1A", linewidth=3) + + # Set bounds to Poland + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_powiat_image_bytes( + powiat_name: str, + powiat_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + all_powiaty: gpd.GeoDataFrame, +) -> bytes: + """Generate a powiat map image as bytes.""" + fig = create_powiat_map(powiat_name, powiat_gdf, poland_boundary, all_powiaty) + + 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( + powiaty: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish Powiaty", +) -> genanki.Package: + """Generate Anki package for Polish powiaty.""" + model_id_hash = hashlib.md5(f"polish_powiaty_{deck_name}".encode()) # noqa: S324 + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +""" + + my_model = genanki.Model( + model_id, + "Polish Powiat Model", + fields=[ + {"name": "PowiatMap"}, + {"name": "PowiatName"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{PowiatMap}}
', + "afmt": '
{{PowiatMap}}
' + '
' + '
{{PowiatName}}
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + for _, row in powiaty.iterrows(): + powiat_name = row["nazwa"] + powiat_gdf = gpd.GeoDataFrame([row], crs=powiaty.crs) + + image_data = generate_powiat_image_bytes( + powiat_name, powiat_gdf, poland_boundary, powiaty + ) + filename = f"powiat_{powiat_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', powiat_name], + tags=["geography", "poland", "powiaty"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish powiaty.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_powiaty.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish Powiaty", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("polish_powiaty.apkg") + + try: + sys.stdout.write("Loading powiaty data...\n") + powiaty = get_polish_powiaty() + poland_boundary = get_poland_boundary() + num_powiaty = len(powiaty) + + sys.stdout.write(f"Generating flashcards for {num_powiaty} powiaty...\n") + + package = generate_anki_package(powiaty, poland_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_powiaty = list(powiaty.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_powiaty)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_powiaty: + powiat_name = row["nazwa"] + powiat_gdf = gpd.GeoDataFrame([row], crs=powiaty.crs) + image_data = generate_powiat_image_bytes( + powiat_name, powiat_gdf, poland_boundary, powiaty + ) + safe_name = powiat_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Powiaty: {num_powiaty}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_powiaty/run.sh b/python_pkg/anki_decks/polish_powiaty/run.sh new file mode 100755 index 0000000..2d5d8a1 --- /dev/null +++ b/python_pkg/anki_decks/polish_powiaty/run.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Script to generate Polish Powiaty Anki deck + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="$SCRIPT_DIR/.venv" +PREVIEW_DIR="$SCRIPT_DIR/preview_images" + +echo "=== Polish Powiaty Anki Generator ===" +echo + +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment..." + python3 -m venv "$VENV_DIR" +fi + +echo "Activating virtual environment..." +source "$VENV_DIR/bin/activate" + +echo "Installing dependencies..." +pip install --quiet --upgrade pip +pip install --quiet matplotlib genanki geopandas + +cd "$SCRIPT_DIR" + +# Create preview images directory +mkdir -p "$PREVIEW_DIR" + +python -m polish_powiaty_anki --output polish_powiaty.apkg --preview "$PREVIEW_DIR" --preview-count 5 + +echo +echo "Done! The Anki deck is at: $SCRIPT_DIR/polish_powiaty.apkg" +echo "Preview images are in: $PREVIEW_DIR" diff --git a/python_pkg/anki_decks/polish_rivers/__init__.py b/python_pkg/anki_decks/polish_rivers/__init__.py new file mode 100644 index 0000000..e9098ab --- /dev/null +++ b/python_pkg/anki_decks/polish_rivers/__init__.py @@ -0,0 +1 @@ +"""Polish rivers Anki deck generator.""" diff --git a/python_pkg/anki_decks/polish_rivers/polish_rivers_anki.py b/python_pkg/anki_decks/polish_rivers/polish_rivers_anki.py new file mode 100644 index 0000000..71461f0 --- /dev/null +++ b/python_pkg/anki_decks/polish_rivers/polish_rivers_anki.py @@ -0,0 +1,355 @@ +"""Anki flashcard generator for Polish rivers. + +Generates Anki-compatible flashcard decks with maps showing rivers +highlighted on a Poland map. +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_rivers + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +RIVER_COLOR = "#2980B9" # Dark blue for rivers +NEIGHBOR_COLOR = "#EAECEE" # Light gray for neighboring areas + + +def create_river_map( + river_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Poland with one river highlighted. + + Rivers that extend beyond Poland show an extended view. + """ + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Get Poland bounds + poland_bounds = poland_boundary.total_bounds + river_bounds = river_gdf.total_bounds + + # Check if river extends beyond Poland + extends_beyond = ( + river_bounds[0] < poland_bounds[0] + or river_bounds[1] < poland_bounds[1] + or river_bounds[2] > poland_bounds[2] + or river_bounds[3] > poland_bounds[3] + ) + + if extends_beyond: + # Calculate extended bounds with some padding + min_x = min(poland_bounds[0], river_bounds[0]) - 0.2 + min_y = min(poland_bounds[1], river_bounds[1]) - 0.2 + max_x = max(poland_bounds[2], river_bounds[2]) + 0.2 + max_y = max(poland_bounds[3], river_bounds[3]) + 0.2 + + # Draw background for extended area (neighboring countries) + ax.fill( + [min_x, max_x, max_x, min_x, min_x], + [min_y, min_y, max_y, max_y, min_y], + color=NEIGHBOR_COLOR, + alpha=0.3, + ) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Plot the river + river_gdf.plot(ax=ax, color=RIVER_COLOR, linewidth=3, alpha=0.9) + + if extends_beyond: + ax.set_xlim(min_x, max_x) + ax.set_ylim(min_y, max_y) + else: + # Set bounds to Poland + ax.set_xlim(poland_bounds[0], poland_bounds[2]) + ax.set_ylim(poland_bounds[1], poland_bounds[3]) + + return fig + + +def generate_river_image_bytes( + river_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> bytes: + """Generate a river map image as bytes.""" + fig = create_river_map(river_gdf, poland_boundary) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None + + +def _init_worker(poland_geojson: str) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + + +def _render_single_river(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single river image (worker function). + + Args: + args: Tuple of (river_name, river_geojson_str). + + Returns: + Tuple of (river_name, image_bytes). + """ + river_name, river_geojson = args + river_gdf = gpd.read_file(river_geojson) + + assert _mp_poland_boundary is not None # noqa: S101 + + image_data = generate_river_image_bytes(river_gdf, _mp_poland_boundary) + return river_name, image_data + + +def generate_anki_package( + rivers: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish Rivers", +) -> genanki.Package: + """Generate Anki package for Polish rivers.""" + model_id_hash = hashlib.md5(f"polish_rivers_{deck_name}".encode()) # noqa: S324 + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.info-text { + font-size: 18px; + color: #7F8C8D; + margin-top: 10px; +} +.card.night_mode .info-text { + color: #BDC3C7; +} +""" + + my_model = genanki.Model( + model_id, + "Polish River Model", + fields=[ + {"name": "RiverMap"}, + {"name": "RiverName"}, + {"name": "Length"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{RiverMap}}
', + "afmt": '
{{RiverMap}}
' + '
' + '
{{RiverName}}
' + '
~{{Length}} km w Polsce
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Prepare data for parallel processing + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (river_name, river_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in rivers.iterrows(): + river_gdf = gpd.GeoDataFrame([row], crs=rivers.crs) + river_geojson = river_gdf.to_json() + work_items.append((row["name"], river_geojson)) + + # Use multiprocessing for parallel rendering + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path,), + ) as pool: + for i, (river_name, image_data) in enumerate( + pool.imap_unordered(_render_single_river, work_items) + ): + results[river_name] = image_data + if (i + 1) % 50 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in rivers.iterrows(): + river_name = row["name"] + length_km = round(row["length_km"]) if "length_km" in row else 0 + image_data = results[river_name] + filename = f"river_{river_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', river_name, str(length_km)], + tags=["geography", "poland", "rivers", "water"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish rivers.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_rivers.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish Rivers", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("polish_rivers.apkg") + + try: + sys.stdout.write("Loading rivers data...\n") + rivers = get_polish_rivers() + poland_boundary = get_poland_boundary() + num_rivers = len(rivers) + + sys.stdout.write(f"Found {num_rivers} rivers.\n") + sys.stdout.write("Generating flashcards...\n") + + package = generate_anki_package(rivers, poland_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_rivers = list(rivers.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_rivers)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_rivers: + river_name = row["name"] + river_gdf = gpd.GeoDataFrame([row], crs=rivers.crs) + image_data = generate_river_image_bytes(river_gdf, poland_boundary) + safe_name = river_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Rivers: {num_rivers}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_rivers/run.sh b/python_pkg/anki_decks/polish_rivers/run.sh new file mode 100755 index 0000000..d84ad5d --- /dev/null +++ b/python_pkg/anki_decks/polish_rivers/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run the Polish rivers Anki generator + +cd "$(dirname "$0")" || exit + +python polish_rivers_anki.py --preview preview_images --preview-count 5 "$@" diff --git a/python_pkg/anki_decks/polish_unesco_sites/__init__.py b/python_pkg/anki_decks/polish_unesco_sites/__init__.py new file mode 100644 index 0000000..7e7cdfa --- /dev/null +++ b/python_pkg/anki_decks/polish_unesco_sites/__init__.py @@ -0,0 +1 @@ +"""Polish UNESCO sites Anki generator.""" diff --git a/python_pkg/anki_decks/polish_unesco_sites/polish_unesco_sites_anki.py b/python_pkg/anki_decks/polish_unesco_sites/polish_unesco_sites_anki.py new file mode 100644 index 0000000..314e6d8 --- /dev/null +++ b/python_pkg/anki_decks/polish_unesco_sites/polish_unesco_sites_anki.py @@ -0,0 +1,365 @@ +"""Anki flashcard generator for Polish UNESCO World Heritage Sites. + +Generates Anki-compatible flashcard decks with maps showing UNESCO sites +highlighted on a Poland map. +""" + +from __future__ import annotations + +import argparse +import hashlib +from io import BytesIO +import multiprocessing as mp +from pathlib import Path +import random +import sys +import tempfile +from typing import TYPE_CHECKING + +import genanki +import geopandas as gpd +import matplotlib as mpl + +mpl.use("Agg") # Non-interactive backend for multiprocessing +import matplotlib.pyplot as plt +from shapely.geometry import Point + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_poland_boundary, get_polish_unesco_sites + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +SITE_COLOR_POLYGON = "#9B59B6" # Purple for polygon sites +SITE_COLOR_POINT = "#9B59B6" # Purple for point markers + + +def create_unesco_map( + site_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Poland with one UNESCO site highlighted. + + Always shows a star marker at the centroid for consistency. + """ + fig, ax = plt.subplots(figsize=(10, 12)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Poland as a plain gray shape + poland_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + poland_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=1) + + # Get centroid for star marker + geom = site_gdf.iloc[0].geometry + if isinstance(geom, Point): + x, y = geom.x, geom.y + else: + centroid = geom.centroid + x, y = centroid.x, centroid.y + + # Always show a star marker for consistency + ax.scatter( + [x], + [y], + s=800, + c=SITE_COLOR_POINT, + marker="*", + edgecolor="#1A1A1A", + linewidth=2, + zorder=10, + ) + + # Set bounds to Poland + bounds = poland_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_unesco_image_bytes( + site_gdf: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, +) -> bytes: + """Generate a UNESCO site map image as bytes.""" + fig = create_unesco_map(site_gdf, poland_boundary) + + buf = BytesIO() + fig.savefig(buf, format="png", bbox_inches="tight", dpi=150) + plt.close(fig) + buf.seek(0) + + return buf.read() + + +# Global variables for multiprocessing (set via initializer) +_mp_poland_boundary: gpd.GeoDataFrame | None = None + + +def _init_worker(poland_geojson: str) -> None: + """Initialize worker process with shared data.""" + global _mp_poland_boundary # noqa: PLW0603 + _mp_poland_boundary = gpd.read_file(poland_geojson) + + +def _render_single_site(args: tuple[str, str]) -> tuple[str, bytes]: + """Render a single site image (worker function). + + Args: + args: Tuple of (site_name, site_geojson_str). + + Returns: + Tuple of (site_name, image_bytes). + """ + site_name, site_geojson = args + site_gdf = gpd.read_file(site_geojson) + + assert _mp_poland_boundary is not None # noqa: S101 + + image_data = generate_unesco_image_bytes(site_gdf, _mp_poland_boundary) + return site_name, image_data + + +def generate_anki_package( + sites: gpd.GeoDataFrame, + poland_boundary: gpd.GeoDataFrame, + deck_name: str = "Polish UNESCO World Heritage Sites", +) -> genanki.Package: + """Generate Anki package for Polish UNESCO sites.""" + model_id_hash = hashlib.md5( # noqa: S324 + f"polish_unesco_sites_{deck_name}".encode() + ) + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 28px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.info-text { + font-size: 18px; + color: #7F8C8D; + margin-top: 10px; +} +.card.night_mode .info-text { + color: #BDC3C7; +} +.year-badge { + display: inline-block; + background: #9B59B6; + color: white; + padding: 4px 12px; + border-radius: 15px; + font-size: 16px; + margin-top: 8px; +} +.card.night_mode .year-badge { + background: #8E44AD; +} +""" + + my_model = genanki.Model( + model_id, + "Polish UNESCO Site Model", + fields=[ + {"name": "SiteMap"}, + {"name": "SiteName"}, + {"name": "InscribedYear"}, + {"name": "Category"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{SiteMap}}
', + "afmt": '
{{SiteMap}}
' + '
' + '
{{SiteName}}
' + '
{{Category}}
' + '
Inscribed: {{InscribedYear}}
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Prepare data for parallel processing + with tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) as f: + poland_boundary.to_file(f.name, driver="GeoJSON") + poland_geojson_path = f.name + + # Prepare work items: (site_name, site_geojson_str) + work_items: list[tuple[str, str]] = [] + for _, row in sites.iterrows(): + site_gdf = gpd.GeoDataFrame([row], crs=sites.crs) + site_geojson = site_gdf.to_json() + work_items.append((row["name"], site_geojson)) + + # Use multiprocessing for parallel rendering + num_workers = min(mp.cpu_count(), 8) + sys.stdout.write( + f"Rendering {len(work_items)} images using {num_workers} workers...\n" + ) + + results: dict[str, bytes] = {} + with mp.Pool( + num_workers, + initializer=_init_worker, + initargs=(poland_geojson_path,), + ) as pool: + for i, (site_name, image_data) in enumerate( + pool.imap_unordered(_render_single_site, work_items) + ): + results[site_name] = image_data + if (i + 1) % 5 == 0: + sys.stdout.write(f" Rendered {i + 1}/{len(work_items)}...\n") + + # Clean up temp file + Path(poland_geojson_path).unlink(missing_ok=True) + + # Create notes from results + for _, row in sites.iterrows(): + site_name = row["name"] + inscribed_year = row.get("inscribed_year", "Unknown") + category = row.get("category", "Cultural/Natural") + image_data = results[site_name] + filename = f"unesco_{site_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[ + f'', + site_name, + str(inscribed_year), + category, + ], + tags=["geography", "poland", "unesco", "heritage"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Polish UNESCO sites.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: polish_unesco_sites.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Polish UNESCO World Heritage Sites", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("polish_unesco_sites.apkg") + + try: + sys.stdout.write("Loading UNESCO sites data...\n") + sites = get_polish_unesco_sites() + poland_boundary = get_poland_boundary() + num_sites = len(sites) + + sys.stdout.write(f"Found {num_sites} UNESCO World Heritage Sites.\n") + sys.stdout.write("Generating flashcards...\n") + + package = generate_anki_package(sites, poland_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_sites = list(sites.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_sites)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_sites: + site_name = row["name"] + site_gdf = gpd.GeoDataFrame([row], crs=sites.crs) + image_data = generate_unesco_image_bytes(site_gdf, poland_boundary) + safe_name = site_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"UNESCO sites: {num_sites}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/polish_unesco_sites/run.sh b/python_pkg/anki_decks/polish_unesco_sites/run.sh new file mode 100755 index 0000000..40c5113 --- /dev/null +++ b/python_pkg/anki_decks/polish_unesco_sites/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run the Polish UNESCO sites Anki generator + +cd "$(dirname "$0")" || exit + +python polish_unesco_sites_anki.py --preview preview_images --preview-count 5 "$@" diff --git a/python_pkg/anki_decks/preview_all.html b/python_pkg/anki_decks/preview_all.html new file mode 100644 index 0000000..f1347a2 --- /dev/null +++ b/python_pkg/anki_decks/preview_all.html @@ -0,0 +1,82 @@ + + + + + All Preview Images + + + +

🗺️ Anki Geography Cards - Preview Images

+ +

🛣️ Warsaw Streets (Top 5 Longest)

+ + +

🚇 Warsaw Metro Stations

+ + +

🌉 Warsaw Bridges

+ + +

🏛️ Warsaw Landmarks

+ + +

🏘️ Warsaw Osiedla

+ + +

🗺️ Polish Powiaty

+ + +

🏙️ Warsaw Districts

+ + + diff --git a/python_pkg/anki_decks/warsaw_bridges/__init__.py b/python_pkg/anki_decks/warsaw_bridges/__init__.py new file mode 100644 index 0000000..4d27c39 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_bridges/__init__.py @@ -0,0 +1 @@ +"""Warsaw bridges Anki flashcard generator.""" diff --git a/python_pkg/anki_decks/warsaw_bridges/run.sh b/python_pkg/anki_decks/warsaw_bridges/run.sh new file mode 100755 index 0000000..c00a15c --- /dev/null +++ b/python_pkg/anki_decks/warsaw_bridges/run.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Script to generate Warsaw Bridges Anki deck + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="$SCRIPT_DIR/.venv" +PREVIEW_DIR="$SCRIPT_DIR/preview_images" + +echo "=== Warsaw Bridges Anki Generator ===" +echo + +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment..." + python3 -m venv "$VENV_DIR" +fi + +echo "Activating virtual environment..." +source "$VENV_DIR/bin/activate" + +echo "Installing dependencies..." +pip install --quiet --upgrade pip +pip install --quiet matplotlib genanki geopandas requests shapely + +cd "$SCRIPT_DIR" + +# Create preview images directory +mkdir -p "$PREVIEW_DIR" + +python -m warsaw_bridges_anki --output warsaw_bridges.apkg --preview "$PREVIEW_DIR" --preview-count 5 + +echo +echo "Done! The Anki deck is at: $SCRIPT_DIR/warsaw_bridges.apkg" +echo "Preview images are in: $PREVIEW_DIR" diff --git a/python_pkg/anki_decks/warsaw_bridges/warsaw_bridges_anki.py b/python_pkg/anki_decks/warsaw_bridges/warsaw_bridges_anki.py new file mode 100755 index 0000000..883ef70 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_bridges/warsaw_bridges_anki.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +"""Anki flashcard generator for Warsaw bridges over the Vistula. + +Generates Anki-compatible flashcard decks with maps showing individual +Warsaw bridges highlighted on a city map. +""" + +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 + +# Import shared data module +sys.path.insert(0, str(Path(__file__).parent.parent)) +from geo_data import get_vistula_river, get_warsaw_bridges + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +# Bridge color +BRIDGE_COLOR = "#E74C3C" # Red +RIVER_COLOR = "#3498DB" # Blue + + +def load_warsaw_boundary() -> gpd.GeoDataFrame: + """Load Warsaw boundary from districts GeoJSON. + + Returns: + GeoDataFrame with Warsaw boundary. + + Raises: + FileNotFoundError: If boundary data file not found. + """ + districts_path = ( + Path(__file__).parent.parent / "warsaw_districts" / "warszawa-dzielnice.geojson" + ) + if districts_path.exists(): + warsaw_gdf = gpd.read_file(districts_path) + warsaw_boundary = warsaw_gdf[warsaw_gdf["name"] == "Warszawa"] + if len(warsaw_boundary) == 0: + warsaw_boundary = gpd.GeoDataFrame( + geometry=[warsaw_gdf.union_all()], crs=warsaw_gdf.crs + ) + return warsaw_boundary + + msg = "Warsaw boundary data not found" + raise FileNotFoundError(msg) + + +def create_bridge_map( + bridge_gdf: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, + vistula: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Warsaw with one bridge highlighted. + + Args: + bridge_gdf: GeoDataFrame with the bridge to highlight. + warsaw_boundary: GeoDataFrame with Warsaw boundary. + vistula: GeoDataFrame with Vistula river geometry. + + Returns: + Matplotlib figure with the map. + """ + fig, ax = plt.subplots(figsize=(10, 10)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Warsaw as a plain gray shape + warsaw_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + warsaw_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=2) + + # Plot Vistula river + vistula.plot(ax=ax, color=RIVER_COLOR, linewidth=3, alpha=0.7) + + # Plot the bridge + bridge_gdf.plot(ax=ax, color=BRIDGE_COLOR, linewidth=6, alpha=0.9) + + # Set bounds to Warsaw + bounds = warsaw_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_bridge_image_bytes( + bridge_gdf: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, + vistula: gpd.GeoDataFrame, +) -> bytes: + """Generate a bridge map image as bytes. + + Args: + bridge_gdf: GeoDataFrame with the bridge. + warsaw_boundary: GeoDataFrame with Warsaw boundary. + vistula: GeoDataFrame with Vistula river. + + Returns: + PNG image bytes. + """ + fig = create_bridge_map(bridge_gdf, warsaw_boundary, vistula) + + 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( + bridges: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, + vistula: gpd.GeoDataFrame, + deck_name: str = "Warsaw Bridges", +) -> genanki.Package: + """Generate Anki package for Warsaw bridges. + + Args: + bridges: GeoDataFrame with all bridges. + warsaw_boundary: GeoDataFrame with Warsaw boundary. + vistula: GeoDataFrame with Vistula river. + deck_name: Name for the Anki deck. + + Returns: + Generated Anki package. + """ + model_id_hash = hashlib.md5(f"warsaw_bridges_{deck_name}".encode()) # noqa: S324 + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +""" + + my_model = genanki.Model( + model_id, + "Warsaw Bridge Model", + fields=[ + {"name": "BridgeMap"}, + {"name": "BridgeName"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{BridgeMap}}
', + "afmt": '
{{BridgeMap}}
' + '
' + '
{{BridgeName}}
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + for _, row in bridges.iterrows(): + bridge_name = row["name"] + bridge_gdf = gpd.GeoDataFrame([row], crs=bridges.crs) + + image_data = generate_bridge_image_bytes(bridge_gdf, warsaw_boundary, vistula) + filename = f"bridge_{bridge_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', bridge_name], + tags=["geography", "warsaw", "bridges"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point. + + Args: + argv: Command-line arguments. + + Returns: + Exit code. + """ + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Warsaw bridges.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: warsaw_bridges.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Warsaw Bridges", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("warsaw_bridges.apkg") + + try: + sys.stdout.write("Loading bridge data...\n") + bridges = get_warsaw_bridges() + vistula = get_vistula_river() + warsaw_boundary = load_warsaw_boundary() + num_bridges = len(bridges) + + sys.stdout.write(f"Generating flashcards for {num_bridges} bridges...\n") + + package = generate_anki_package( + bridges, warsaw_boundary, vistula, args.deck_name + ) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_bridges = list(bridges.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_bridges)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_bridges: + bridge_name = row["name"] + bridge_gdf = gpd.GeoDataFrame([row], crs=bridges.crs) + image_data = generate_bridge_image_bytes( + bridge_gdf, warsaw_boundary, vistula + ) + safe_name = bridge_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Bridges: {num_bridges}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/warsaw_districts/README.md b/python_pkg/anki_decks/warsaw_districts/README.md new file mode 100644 index 0000000..a16dd6b --- /dev/null +++ b/python_pkg/anki_decks/warsaw_districts/README.md @@ -0,0 +1,120 @@ +# Warsaw Districts Anki Generator + +Generate Anki flashcards for learning the 18 districts (dzielnice) of Warsaw, Poland. + +## Features + +- Generates flashcards for all 18 Warsaw districts +- **Uses real district boundaries from OpenStreetMap data** +- Front of card: Map showing the full city with only the target district's border highlighted in bold +- Back of card: District name in Polish +- Self-contained .apkg file with embedded images +- Compatible with AnkiWeb and AnkiDroid + +## Data Source + +District boundaries are sourced from [andilabs/warszawa-dzielnice-geojson](https://github.com/andilabs/warszawa-dzielnice-geojson), which provides accurate OpenStreetMap-based GeoJSON data for all Warsaw districts. + +## Installation + +Install dependencies using your preferred method: + +### Using pyenv (recommended) + +```bash +pyenv install 3.10 # or later +pyenv shell 3.10 +pip install matplotlib genanki geopandas +``` + +### Using pipx + +```bash +pipx install --python python3.10 matplotlib genanki geopandas +``` + +### Using system package manager (Arch Linux) + +```bash +sudo pacman -S python-matplotlib python-geopandas +pip install genanki +``` + +### Using pip directly + +```bash +pip install matplotlib genanki geopandas +``` + +## Usage + +### Generate flashcards + +```bash +# From the repository root +python -m python_pkg.anki_decks.warsaw_districts.warsaw_districts_anki +``` + +This creates: + +- `warsaw_districts.apkg` - Self-contained Anki package with all images embedded + +### Custom options + +```bash +# Custom output file +python -m python_pkg.anki_decks.warsaw_districts.warsaw_districts_anki --output my_cards.apkg + +# Custom deck name +python -m python_pkg.anki_decks.warsaw_districts.warsaw_districts_anki --deck-name "Warszawa - Dzielnice" +``` + +## Importing into Anki + +1. Open Anki +2. File → Import +3. Select the generated `warsaw_districts.apkg` file +4. Click Import + +That's it! All images are already embedded in the .apkg file. + +## Warsaw Districts + +The generator includes all 18 official districts of Warsaw: + +1. Bemowo +2. Białołęka +3. Bielany +4. Mokotów +5. Ochota +6. Praga-Południe +7. Praga-Północ +8. Rembertów +9. Śródmieście +10. Targówek +11. Ursus +12. Ursynów +13. Wawer +14. Wesoła +15. Wilanów +16. Włochy +17. Wola +18. Żoliborz + +## Development + +### Running tests + +```bash +pytest python_pkg/warsaw_districts/tests/ +``` + +### Code quality + +```bash +ruff check python_pkg/warsaw_districts/ +``` + +## License + +Same as the parent repository. diff --git a/python_pkg/anki_decks/warsaw_districts/__init__.py b/python_pkg/anki_decks/warsaw_districts/__init__.py new file mode 100644 index 0000000..efb0fdd --- /dev/null +++ b/python_pkg/anki_decks/warsaw_districts/__init__.py @@ -0,0 +1,5 @@ +"""Warsaw districts Anki flashcard generator.""" + +from __future__ import annotations + +__all__ = ["generate_anki_deck", "main"] diff --git a/python_pkg/anki_decks/warsaw_districts/run.sh b/python_pkg/anki_decks/warsaw_districts/run.sh new file mode 100755 index 0000000..cf60e60 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_districts/run.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Script to set up environment, install dependencies, and generate Warsaw Districts Anki deck + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="$SCRIPT_DIR/.venv" +PREVIEW_DIR="$SCRIPT_DIR/preview_images" + +echo "=== Warsaw Districts Anki Generator ===" +echo + +# Create virtual environment if it doesn't exist +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment..." + python3 -m venv "$VENV_DIR" +fi + +# Activate virtual environment +echo "Activating virtual environment..." +source "$VENV_DIR/bin/activate" + +# Install dependencies +echo "Installing dependencies..." +pip install --quiet --upgrade pip +pip install --quiet matplotlib genanki geopandas + +# Export preview images +echo +echo "Exporting preview images to $PREVIEW_DIR..." +mkdir -p "$PREVIEW_DIR" +cd "$SCRIPT_DIR" +python -c " +from warsaw_districts_anki import WARSAW_DISTRICTS, generate_district_image_bytes +from pathlib import Path + +preview_dir = Path('$PREVIEW_DIR') +for district in WARSAW_DISTRICTS: + filename = district.replace(' ', '_').replace('-', '_') + '.png' + filepath = preview_dir / filename + filepath.write_bytes(generate_district_image_bytes(district)) + print(f' Exported: {filename}') +" + +echo +echo "Preview images exported! Check: $PREVIEW_DIR" + +# Generate Anki deck +echo +echo "Generating Anki flashcards..." +python -m warsaw_districts_anki --output warsaw_districts.apkg + +echo +echo "Done!" +echo " - Preview images: $PREVIEW_DIR" +echo " - Anki deck: $SCRIPT_DIR/warsaw_districts.apkg" diff --git a/python_pkg/anki_decks/warsaw_districts/tests/__init__.py b/python_pkg/anki_decks/warsaw_districts/tests/__init__.py new file mode 100644 index 0000000..7b53cd4 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_districts/tests/__init__.py @@ -0,0 +1,5 @@ +"""Tests init file.""" + +from __future__ import annotations + +__all__: list[str] = [] diff --git a/python_pkg/anki_decks/warsaw_districts/tests/test_warsaw_districts_anki.py b/python_pkg/anki_decks/warsaw_districts/tests/test_warsaw_districts_anki.py new file mode 100644 index 0000000..c1592e5 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_districts/tests/test_warsaw_districts_anki.py @@ -0,0 +1,175 @@ +"""Tests for the Warsaw districts Anki generator.""" + +from __future__ import annotations + +from pathlib import Path + +import matplotlib.pyplot as plt +import pytest + +try: + from python_pkg.anki_decks.warsaw_districts.warsaw_districts_anki import ( + WARSAW_DISTRICTS, + create_district_map, + generate_anki_package, + generate_district_image_bytes, + main, + ) +except ImportError: + import sys + + sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent)) + from python_pkg.anki_decks.warsaw_districts.warsaw_districts_anki import ( + WARSAW_DISTRICTS, + create_district_map, + generate_anki_package, + generate_district_image_bytes, + main, + ) + + +class TestDistricts: + """Tests for Warsaw districts data.""" + + def test_has_18_districts(self) -> None: + """Test that we have exactly 18 Warsaw districts.""" + assert len(WARSAW_DISTRICTS) == 18 + + def test_all_districts_are_strings(self) -> None: + """Test that all district entries are strings.""" + for district in WARSAW_DISTRICTS: + assert isinstance(district, str) + assert len(district) > 0 + + def test_districts_are_unique(self) -> None: + """Test that all district names are unique.""" + assert len(WARSAW_DISTRICTS) == len(set(WARSAW_DISTRICTS)) + + def test_known_districts_present(self) -> None: + """Test that all known Warsaw districts are in the list.""" + district_set = set(WARSAW_DISTRICTS) + # Check all 18 districts + expected_districts = { + "Bemowo", + "Białołęka", + "Bielany", + "Mokotów", + "Ochota", + "Praga Południe", # Note: space, not hyphen + "Praga Północ", # Note: space, not hyphen + "Rembertów", + "Śródmieście", + "Targówek", + "Ursus", + "Ursynów", + "Wawer", + "Wesoła", + "Wilanów", + "Włochy", + "Wola", + "Żoliborz", + } + assert district_set == expected_districts + + +class TestCreateDistrictMap: + """Tests for creating district maps.""" + + def test_creates_figure(self) -> None: + """Test that create_district_map returns a Figure.""" + district = WARSAW_DISTRICTS[0] + fig = create_district_map(district) + assert fig is not None + # Clean up + plt.close(fig) + + def test_creates_figure_for_all_districts(self) -> None: + """Test that we can create maps for all districts.""" + for district in WARSAW_DISTRICTS: + fig = create_district_map(district) + assert fig is not None + plt.close(fig) + + +class TestGenerateDistrictImageBytes: + """Tests for generating district image bytes.""" + + def test_generates_bytes(self) -> None: + """Test that generate_district_image_bytes returns bytes.""" + district = WARSAW_DISTRICTS[0] + image_bytes = generate_district_image_bytes(district) + assert isinstance(image_bytes, bytes) + assert len(image_bytes) > 0 + + def test_generates_for_all_districts(self) -> None: + """Test that we can generate images for all districts.""" + for district in WARSAW_DISTRICTS: + image_bytes = generate_district_image_bytes(district) + assert isinstance(image_bytes, bytes) + assert len(image_bytes) > 0 + + +class TestGenerateAnkiPackage: + """Tests for generating Anki package.""" + + def test_generates_package(self) -> None: + """Test that output is a genanki Package.""" + package = generate_anki_package("Test Deck") + assert package is not None + assert len(package.decks) == 1 + + def test_generates_notes_for_all_districts(self) -> None: + """Test that package contains cards for all 18 districts.""" + package = generate_anki_package() + deck = package.decks[0] + assert len(deck.notes) == len(WARSAW_DISTRICTS) + + def test_custom_deck_name(self) -> None: + """Test that custom deck name is used.""" + package = generate_anki_package("Custom Deck") + deck = package.decks[0] + assert deck.name == "Custom Deck" + + +class TestMain: + """Tests for the main CLI function.""" + + def test_creates_output_file(self, tmp_path: Path) -> None: + """Test that main creates the output file.""" + output_file = tmp_path / "test_output.apkg" + + result = main( + [ + "--output", + str(output_file), + ] + ) + + assert result == 0 + assert output_file.exists() + + def test_custom_deck_name(self, tmp_path: Path) -> None: + """Test that custom deck name is used.""" + output_file = tmp_path / "test_output.apkg" + + result = main( + [ + "--output", + str(output_file), + "--deck-name", + "Custom Deck", + ] + ) + + assert result == 0 + assert output_file.exists() + + def test_help_flag(self) -> None: + """Test that --help works.""" + with pytest.raises(SystemExit) as exc_info: + main(["--help"]) + assert exc_info.value.code == 0 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/python_pkg/anki_decks/warsaw_districts/warsaw_districts.apkg b/python_pkg/anki_decks/warsaw_districts/warsaw_districts.apkg new file mode 100644 index 0000000..843434a Binary files /dev/null and b/python_pkg/anki_decks/warsaw_districts/warsaw_districts.apkg differ diff --git a/python_pkg/anki_decks/warsaw_districts/warsaw_districts_anki.py b/python_pkg/anki_decks/warsaw_districts/warsaw_districts_anki.py new file mode 100755 index 0000000..2289f2d --- /dev/null +++ b/python_pkg/anki_decks/warsaw_districts/warsaw_districts_anki.py @@ -0,0 +1,356 @@ +#!/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.anki_decks.warsaw_districts.warsaw_districts_anki + + # Specify custom output file + python -m python_pkg.anki_decks.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() + + +# 18 unique distinct colors for all Warsaw districts +# Chosen to be visually distinct from each other in both light and dark modes +DISTRICT_COLORS = [ + "#E74C3C", # Red + "#3498DB", # Blue + "#2ECC71", # Emerald green + "#9B59B6", # Purple + "#F39C12", # Orange + "#1ABC9C", # Turquoise + "#E91E63", # Pink + "#00BCD4", # Cyan + "#8BC34A", # Light green + "#FF5722", # Deep orange + "#673AB7", # Deep purple + "#FFEB3B", # Yellow + "#795548", # Brown + "#607D8B", # Blue grey + "#CDDC39", # Lime + "#FF9800", # Amber + "#4CAF50", # Green + "#03A9F4", # Light blue +] + + +def create_district_map(district_name: str) -> Figure: + """Create a map showing Warsaw with one district highlighted. + + The map shows Warsaw as a plain shape (no internal district borders) + with only the target district highlighted in color with a bold border. + This makes it harder to guess the district using contextual cues. + + Args: + district_name: Name of the district to highlight. + + Returns: + A matplotlib Figure object. + """ + # Load all district data + gdf = load_district_data() + + # Create figure with transparent background + fig, ax = plt.subplots(figsize=(10, 10)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Find 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) + + # Create unified Warsaw shape by dissolving all districts + warsaw_unified = gdf.union_all() + + # Plot Warsaw as a plain gray shape (no internal borders) + warsaw_gdf = gpd.GeoDataFrame(geometry=[warsaw_unified], crs=gdf.crs) + warsaw_gdf.plot(ax=ax, color="#D5D8DC", alpha=0.6) + warsaw_gdf.boundary.plot(ax=ax, color="#2C3E50", linewidth=2) + + # Assign colors to districts based on sorted names for consistency + sorted_names = sorted(gdf["name"].tolist()) + color_map = { + name: DISTRICT_COLORS[i % len(DISTRICT_COLORS)] + for i, name in enumerate(sorted_names) + } + + # Highlight only the target district with bright color and bold border + fill_color = color_map[district_name] + target.plot(ax=ax, color=fill_color, alpha=0.9) + target.boundary.plot(ax=ax, color="#1A1A1A", linewidth=4) + + # 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) with centered styling + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +""" + + my_model = genanki.Model( + model_id, + "Warsaw District Model", + fields=[ + {"name": "DistrictMap"}, + {"name": "DistrictName"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{DistrictMap}}
', + "afmt": '
{{DistrictMap}}
' + '
' + '
{{DistrictName}}
', + }, + ], + css=card_css, + ) + + # Create a unique deck ID based on deck name + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + + # Create the deck + my_deck = genanki.Deck(deck_id, deck_name) + + # Store media files + media_files = [] + + # Generate notes for each district + for district_name in WARSAW_DISTRICTS: + # Generate image + image_data = generate_district_image_bytes(district_name) + + # Create unique filename + filename = f"{district_name.replace(' ', '_').replace('-', '_')}.png" + + # Create note + note = genanki.Note( + model=my_model, + fields=[ + f'', + district_name, + ], + tags=["geography", "warsaw", "poland"], + ) + + my_deck.add_note(note) + + # Save image data to temporary file for packaging + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + # Create package + package = genanki.Package(my_deck) + package.media_files = media_files + + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point. + + Args: + argv: Command line arguments. + + Returns: + Exit code. + """ + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Warsaw districts.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: warsaw_districts.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Warsaw Districts", + help="Name for the Anki deck (default: 'Warsaw Districts')", + ) + + args = parser.parse_args(argv) + + # Determine output path + output_path = Path(args.output) if args.output else Path("warsaw_districts.apkg") + + try: + num_districts = len(WARSAW_DISTRICTS) + sys.stdout.write( + f"Generating flashcards for {num_districts} Warsaw districts...\n" + ) + sys.stdout.write("Using real district boundaries from OpenStreetMap data.\n") + + # Generate the package + package = generate_anki_package(args.deck_name) + + # Write to file + package.write_to_file(str(output_path)) + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Districts: {num_districts}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + sys.stdout.write("\n") + sys.stdout.write("To import into Anki:\n") + sys.stdout.write(" 1. Open Anki\n") + sys.stdout.write(" 2. File -> Import\n") + sys.stdout.write(f" 3. Select: {output_path.absolute()}\n") + sys.stdout.write(" 4. Click Import\n") + sys.stdout.write("\n") + sys.stdout.write("All images are embedded in the .apkg file!\n") + except (OSError, ValueError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/warsaw_districts/warszawa-dzielnice.geojson b/python_pkg/anki_decks/warsaw_districts/warszawa-dzielnice.geojson new file mode 100644 index 0000000..764a1ce --- /dev/null +++ b/python_pkg/anki_decks/warsaw_districts/warszawa-dzielnice.geojson @@ -0,0 +1,15849 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.02753, + 52.365133 + ], + [ + 21.02539, + 52.36337 + ], + [ + 21.024965, + 52.360891 + ], + [ + 21.024293, + 52.359968 + ], + [ + 21.023439, + 52.357431 + ], + [ + 21.02154, + 52.356737 + ], + [ + 21.020709, + 52.356961 + ], + [ + 21.013644, + 52.356036 + ], + [ + 21.013343, + 52.356826 + ], + [ + 21.00995, + 52.356796 + ], + [ + 21.008849, + 52.357134 + ], + [ + 21.008297, + 52.358211 + ], + [ + 21.008299, + 52.360131 + ], + [ + 21.008199, + 52.360158 + ], + [ + 21.005671, + 52.364096 + ], + [ + 21.004384, + 52.363847 + ], + [ + 21.003231, + 52.363649 + ], + [ + 21.002116, + 52.363581 + ], + [ + 20.99997, + 52.363276 + ], + [ + 20.99943, + 52.363155 + ], + [ + 20.998974, + 52.362878 + ], + [ + 20.998582, + 52.362656 + ], + [ + 20.997026, + 52.362289 + ], + [ + 20.996356, + 52.362089 + ], + [ + 20.99504, + 52.3619 + ], + [ + 20.994671, + 52.361823 + ], + [ + 20.991938, + 52.362769 + ], + [ + 20.981835, + 52.359978 + ], + [ + 20.978142, + 52.358973 + ], + [ + 20.971273, + 52.358976 + ], + [ + 20.970824, + 52.359853 + ], + [ + 20.966102, + 52.368153 + ], + [ + 20.954236, + 52.367857 + ], + [ + 20.953542, + 52.36776 + ], + [ + 20.95331, + 52.367659 + ], + [ + 20.95322, + 52.367597 + ], + [ + 20.952986, + 52.367261 + ], + [ + 20.952658, + 52.366524 + ], + [ + 20.952282, + 52.36645 + ], + [ + 20.951738, + 52.366492 + ], + [ + 20.950893, + 52.366451 + ], + [ + 20.950413, + 52.366416 + ], + [ + 20.949803, + 52.366397 + ], + [ + 20.948179, + 52.366455 + ], + [ + 20.944891, + 52.366697 + ], + [ + 20.94517, + 52.366218 + ], + [ + 20.943994, + 52.365972 + ], + [ + 20.943621, + 52.36636 + ], + [ + 20.940953, + 52.363773 + ], + [ + 20.93991, + 52.362491 + ], + [ + 20.93929, + 52.36177 + ], + [ + 20.935835, + 52.362973 + ], + [ + 20.935478, + 52.363 + ], + [ + 20.935088, + 52.36324 + ], + [ + 20.935859, + 52.36232 + ], + [ + 20.939195, + 52.358356 + ], + [ + 20.938873, + 52.358254 + ], + [ + 20.938499, + 52.358114 + ], + [ + 20.93592, + 52.35891 + ], + [ + 20.935542, + 52.359057 + ], + [ + 20.935104, + 52.359271 + ], + [ + 20.935033, + 52.359295 + ], + [ + 20.933973, + 52.359768 + ], + [ + 20.933093, + 52.359167 + ], + [ + 20.932901, + 52.359193 + ], + [ + 20.93167, + 52.358422 + ], + [ + 20.931419, + 52.358239 + ], + [ + 20.930927, + 52.357937 + ], + [ + 20.930136, + 52.358443 + ], + [ + 20.929269, + 52.358941 + ], + [ + 20.928368, + 52.359195 + ], + [ + 20.927491, + 52.359468 + ], + [ + 20.926863, + 52.359571 + ], + [ + 20.926343, + 52.359627 + ], + [ + 20.924082, + 52.359578 + ], + [ + 20.922972, + 52.357041 + ], + [ + 20.915136, + 52.35403 + ], + [ + 20.912431, + 52.352975 + ], + [ + 20.916528, + 52.349326 + ], + [ + 20.920064, + 52.346643 + ], + [ + 20.921461, + 52.345484 + ], + [ + 20.923335, + 52.343631 + ], + [ + 20.924969, + 52.340403 + ], + [ + 20.924989, + 52.338983 + ], + [ + 20.925398, + 52.337509 + ], + [ + 20.925778, + 52.336104 + ], + [ + 20.925641, + 52.334623 + ], + [ + 20.925123, + 52.32943 + ], + [ + 20.922585, + 52.326754 + ], + [ + 20.922351, + 52.325106 + ], + [ + 20.922246, + 52.324369 + ], + [ + 20.922181, + 52.324303 + ], + [ + 20.922058, + 52.323735 + ], + [ + 20.91307, + 52.321838 + ], + [ + 20.910549, + 52.324096 + ], + [ + 20.910237, + 52.323972 + ], + [ + 20.90896, + 52.322659 + ], + [ + 20.907769, + 52.322226 + ], + [ + 20.907755, + 52.322243 + ], + [ + 20.90709, + 52.322121 + ], + [ + 20.906929, + 52.322689 + ], + [ + 20.906616, + 52.323363 + ], + [ + 20.902956, + 52.322468 + ], + [ + 20.899556, + 52.322197 + ], + [ + 20.897715, + 52.321917 + ], + [ + 20.896036, + 52.321684 + ], + [ + 20.895227, + 52.321438 + ], + [ + 20.894953, + 52.321269 + ], + [ + 20.893015, + 52.319222 + ], + [ + 20.891479, + 52.317625 + ], + [ + 20.890825, + 52.316389 + ], + [ + 20.890356, + 52.315596 + ], + [ + 20.890095, + 52.31496 + ], + [ + 20.889873, + 52.314441 + ], + [ + 20.889736, + 52.314201 + ], + [ + 20.889671, + 52.314054 + ], + [ + 20.889659, + 52.313964 + ], + [ + 20.889759, + 52.313727 + ], + [ + 20.889796, + 52.313681 + ], + [ + 20.889789, + 52.31364 + ], + [ + 20.888649, + 52.313303 + ], + [ + 20.888434, + 52.313238 + ], + [ + 20.886909, + 52.3132 + ], + [ + 20.879799, + 52.313824 + ], + [ + 20.876346, + 52.314881 + ], + [ + 20.876208, + 52.313955 + ], + [ + 20.879267, + 52.311665 + ], + [ + 20.880915, + 52.30918 + ], + [ + 20.880142, + 52.30839 + ], + [ + 20.878719, + 52.304907 + ], + [ + 20.876185, + 52.298703 + ], + [ + 20.876497, + 52.297926 + ], + [ + 20.87647, + 52.297245 + ], + [ + 20.876388, + 52.29661 + ], + [ + 20.876713, + 52.295454 + ], + [ + 20.877172, + 52.29426 + ], + [ + 20.877148, + 52.294061 + ], + [ + 20.876347, + 52.287416 + ], + [ + 20.875787, + 52.280459 + ], + [ + 20.881175, + 52.279653 + ], + [ + 20.880149, + 52.273317 + ], + [ + 20.879683, + 52.271263 + ], + [ + 20.87887, + 52.266292 + ], + [ + 20.878783, + 52.265448 + ], + [ + 20.878689, + 52.265233 + ], + [ + 20.875555, + 52.262149 + ], + [ + 20.874951, + 52.261848 + ], + [ + 20.875101, + 52.261487 + ], + [ + 20.874838, + 52.261229 + ], + [ + 20.873237, + 52.259659 + ], + [ + 20.867329, + 52.253815 + ], + [ + 20.863919, + 52.250441 + ], + [ + 20.863679, + 52.250035 + ], + [ + 20.865665, + 52.249765 + ], + [ + 20.866581, + 52.249605 + ], + [ + 20.86699, + 52.249545 + ], + [ + 20.867732, + 52.249424 + ], + [ + 20.869345, + 52.249418 + ], + [ + 20.872265, + 52.249344 + ], + [ + 20.872712, + 52.249292 + ], + [ + 20.873736, + 52.249094 + ], + [ + 20.874492, + 52.248925 + ], + [ + 20.875834, + 52.248676 + ], + [ + 20.877057, + 52.248503 + ], + [ + 20.877833, + 52.248395 + ], + [ + 20.878808, + 52.248269 + ], + [ + 20.87994, + 52.248088 + ], + [ + 20.881002, + 52.247949 + ], + [ + 20.881963, + 52.247817 + ], + [ + 20.883502, + 52.247451 + ], + [ + 20.883635, + 52.24742 + ], + [ + 20.883875, + 52.247315 + ], + [ + 20.88463, + 52.248173 + ], + [ + 20.885027, + 52.247995 + ], + [ + 20.885011, + 52.247297 + ], + [ + 20.88763, + 52.247268 + ], + [ + 20.890771, + 52.247051 + ], + [ + 20.891077, + 52.246279 + ], + [ + 20.891242, + 52.245345 + ], + [ + 20.891385, + 52.245008 + ], + [ + 20.892076, + 52.244586 + ], + [ + 20.893152, + 52.244126 + ], + [ + 20.893843, + 52.243902 + ], + [ + 20.893778, + 52.243479 + ], + [ + 20.893752, + 52.243313 + ], + [ + 20.893623, + 52.242469 + ], + [ + 20.893016, + 52.239853 + ], + [ + 20.889546, + 52.232047 + ], + [ + 20.887277, + 52.232161 + ], + [ + 20.881921, + 52.232093 + ], + [ + 20.88068, + 52.232053 + ], + [ + 20.880398, + 52.232044 + ], + [ + 20.879732, + 52.232022 + ], + [ + 20.879547, + 52.231909 + ], + [ + 20.877977, + 52.23094 + ], + [ + 20.873101, + 52.229084 + ], + [ + 20.872903, + 52.22894 + ], + [ + 20.872786, + 52.22883 + ], + [ + 20.872712, + 52.228679 + ], + [ + 20.872689, + 52.228534 + ], + [ + 20.872725, + 52.225805 + ], + [ + 20.872682, + 52.225687 + ], + [ + 20.872619, + 52.22566 + ], + [ + 20.872537, + 52.225668 + ], + [ + 20.872699, + 52.225515 + ], + [ + 20.872891, + 52.225381 + ], + [ + 20.873099, + 52.225252 + ], + [ + 20.873205, + 52.225199 + ], + [ + 20.87344, + 52.225089 + ], + [ + 20.873657, + 52.225017 + ], + [ + 20.87406, + 52.224937 + ], + [ + 20.87528, + 52.224803 + ], + [ + 20.87463, + 52.21665 + ], + [ + 20.873086, + 52.216665 + ], + [ + 20.873071, + 52.216546 + ], + [ + 20.873063, + 52.216489 + ], + [ + 20.873055, + 52.216444 + ], + [ + 20.87305, + 52.216415 + ], + [ + 20.872754, + 52.215232 + ], + [ + 20.87194, + 52.215264 + ], + [ + 20.87143, + 52.21316 + ], + [ + 20.871091, + 52.211178 + ], + [ + 20.871016, + 52.210382 + ], + [ + 20.870159, + 52.210391 + ], + [ + 20.870203, + 52.209857 + ], + [ + 20.870899, + 52.208853 + ], + [ + 20.870584, + 52.20884 + ], + [ + 20.870566, + 52.208844 + ], + [ + 20.869921, + 52.208761 + ], + [ + 20.868649, + 52.208627 + ], + [ + 20.865805, + 52.208307 + ], + [ + 20.864618, + 52.208151 + ], + [ + 20.864652, + 52.208057 + ], + [ + 20.862685, + 52.20782 + ], + [ + 20.86267, + 52.207867 + ], + [ + 20.862654, + 52.207913 + ], + [ + 20.861089, + 52.207721 + ], + [ + 20.859603, + 52.20745 + ], + [ + 20.858724, + 52.207263 + ], + [ + 20.858565, + 52.207211 + ], + [ + 20.858076, + 52.207098 + ], + [ + 20.85743, + 52.206907 + ], + [ + 20.856791, + 52.206681 + ], + [ + 20.856158, + 52.206423 + ], + [ + 20.855591, + 52.206144 + ], + [ + 20.855079, + 52.205865 + ], + [ + 20.854554, + 52.205539 + ], + [ + 20.854112, + 52.205224 + ], + [ + 20.853631, + 52.204835 + ], + [ + 20.853221, + 52.204456 + ], + [ + 20.852917, + 52.204101 + ], + [ + 20.852897, + 52.20406 + ], + [ + 20.853319, + 52.204079 + ], + [ + 20.853335, + 52.203993 + ], + [ + 20.852765, + 52.203925 + ], + [ + 20.852591, + 52.203691 + ], + [ + 20.852324, + 52.203289 + ], + [ + 20.852103, + 52.202863 + ], + [ + 20.851986, + 52.202352 + ], + [ + 20.851911, + 52.202019 + ], + [ + 20.851869, + 52.201827 + ], + [ + 20.851856, + 52.201762 + ], + [ + 20.851688, + 52.200977 + ], + [ + 20.851699, + 52.200838 + ], + [ + 20.851709, + 52.200723 + ], + [ + 20.851716, + 52.200681 + ], + [ + 20.851769, + 52.200276 + ], + [ + 20.851882, + 52.199832 + ], + [ + 20.852163, + 52.199792 + ], + [ + 20.852158, + 52.199721 + ], + [ + 20.851917, + 52.199717 + ], + [ + 20.852018, + 52.199438 + ], + [ + 20.852069, + 52.199319 + ], + [ + 20.852723, + 52.197859 + ], + [ + 20.853044, + 52.197258 + ], + [ + 20.853303, + 52.197266 + ], + [ + 20.853306, + 52.197236 + ], + [ + 20.853065, + 52.197195 + ], + [ + 20.853819, + 52.195261 + ], + [ + 20.854239, + 52.194568 + ], + [ + 20.854647, + 52.193975 + ], + [ + 20.856199, + 52.19204 + ], + [ + 20.856685, + 52.191299 + ], + [ + 20.856905, + 52.190439 + ], + [ + 20.856767, + 52.189131 + ], + [ + 20.856445, + 52.188537 + ], + [ + 20.856057, + 52.187973 + ], + [ + 20.855934, + 52.18787 + ], + [ + 20.856034, + 52.187702 + ], + [ + 20.861457, + 52.189316 + ], + [ + 20.861664, + 52.189232 + ], + [ + 20.86335, + 52.18963 + ], + [ + 20.867093, + 52.190765 + ], + [ + 20.867202, + 52.182496 + ], + [ + 20.869159, + 52.18259 + ], + [ + 20.869487, + 52.18259 + ], + [ + 20.870628, + 52.182495 + ], + [ + 20.871509, + 52.182487 + ], + [ + 20.872605, + 52.182355 + ], + [ + 20.874522, + 52.182339 + ], + [ + 20.874813, + 52.182362 + ], + [ + 20.878698, + 52.182522 + ], + [ + 20.879765, + 52.18255 + ], + [ + 20.880261, + 52.182528 + ], + [ + 20.881145, + 52.182488 + ], + [ + 20.881958, + 52.182479 + ], + [ + 20.88565, + 52.182552 + ], + [ + 20.892337, + 52.182525 + ], + [ + 20.897733, + 52.182521 + ], + [ + 20.899275, + 52.184131 + ], + [ + 20.899754, + 52.184201 + ], + [ + 20.901475, + 52.18442 + ], + [ + 20.90159, + 52.184417 + ], + [ + 20.902087, + 52.184251 + ], + [ + 20.902186, + 52.184173 + ], + [ + 20.90266, + 52.18402 + ], + [ + 20.902706, + 52.184068 + ], + [ + 20.903928, + 52.18397 + ], + [ + 20.903971, + 52.184114 + ], + [ + 20.904079, + 52.184118 + ], + [ + 20.909038, + 52.184544 + ], + [ + 20.909088, + 52.184938 + ], + [ + 20.909157, + 52.185296 + ], + [ + 20.909178, + 52.185627 + ], + [ + 20.909209, + 52.185858 + ], + [ + 20.91045, + 52.185813 + ], + [ + 20.910853, + 52.18579 + ], + [ + 20.911347, + 52.18578 + ], + [ + 20.913873, + 52.185851 + ], + [ + 20.914769, + 52.185843 + ], + [ + 20.913388, + 52.18346 + ], + [ + 20.911534, + 52.180093 + ], + [ + 20.911299, + 52.179726 + ], + [ + 20.911216, + 52.17956 + ], + [ + 20.911819, + 52.179444 + ], + [ + 20.914079, + 52.179018 + ], + [ + 20.91703, + 52.178452 + ], + [ + 20.916768, + 52.177806 + ], + [ + 20.917638, + 52.177655 + ], + [ + 20.918737, + 52.177447 + ], + [ + 20.918408, + 52.177057 + ], + [ + 20.918748, + 52.176977 + ], + [ + 20.918992, + 52.176871 + ], + [ + 20.919107, + 52.176751 + ], + [ + 20.919156, + 52.176476 + ], + [ + 20.919117, + 52.174282 + ], + [ + 20.919149, + 52.174011 + ], + [ + 20.919152, + 52.173322 + ], + [ + 20.919159, + 52.171576 + ], + [ + 20.919158, + 52.171083 + ], + [ + 20.919543, + 52.17105 + ], + [ + 20.920332, + 52.170996 + ], + [ + 20.920913, + 52.170966 + ], + [ + 20.921076, + 52.170864 + ], + [ + 20.921335, + 52.170704 + ], + [ + 20.921591, + 52.170545 + ], + [ + 20.921931, + 52.170149 + ], + [ + 20.922252, + 52.169779 + ], + [ + 20.922494, + 52.169726 + ], + [ + 20.922842, + 52.169562 + ], + [ + 20.922828, + 52.169462 + ], + [ + 20.923427, + 52.169262 + ], + [ + 20.923383, + 52.168988 + ], + [ + 20.92318, + 52.168868 + ], + [ + 20.923577, + 52.168766 + ], + [ + 20.92474, + 52.168445 + ], + [ + 20.924741, + 52.167886 + ], + [ + 20.924801, + 52.167693 + ], + [ + 20.924981, + 52.167474 + ], + [ + 20.925369, + 52.167226 + ], + [ + 20.923896, + 52.166157 + ], + [ + 20.925642, + 52.165616 + ], + [ + 20.927377, + 52.16503 + ], + [ + 20.928921, + 52.164544 + ], + [ + 20.930456, + 52.163954 + ], + [ + 20.929951, + 52.163323 + ], + [ + 20.932938, + 52.162018 + ], + [ + 20.934976, + 52.161202 + ], + [ + 20.937237, + 52.160296 + ], + [ + 20.93773, + 52.16013 + ], + [ + 20.938832, + 52.159802 + ], + [ + 20.942639, + 52.158643 + ], + [ + 20.94303, + 52.15854 + ], + [ + 20.944506, + 52.158248 + ], + [ + 20.94516, + 52.158061 + ], + [ + 20.946541, + 52.157374 + ], + [ + 20.947027, + 52.157173 + ], + [ + 20.947765, + 52.156999 + ], + [ + 20.953786, + 52.15615 + ], + [ + 20.953765, + 52.15299 + ], + [ + 20.953764, + 52.152951 + ], + [ + 20.954758, + 52.152537 + ], + [ + 20.957751, + 52.150454 + ], + [ + 20.958437, + 52.149904 + ], + [ + 20.962434, + 52.148089 + ], + [ + 20.964939, + 52.147192 + ], + [ + 20.96965, + 52.145377 + ], + [ + 20.970544, + 52.145194 + ], + [ + 20.97375, + 52.144537 + ], + [ + 20.975877, + 52.144128 + ], + [ + 20.97755, + 52.14349 + ], + [ + 20.98297, + 52.140431 + ], + [ + 20.984737, + 52.139196 + ], + [ + 20.97923, + 52.133852 + ], + [ + 20.979607, + 52.132763 + ], + [ + 20.979608, + 52.129474 + ], + [ + 20.979608, + 52.127582 + ], + [ + 20.98252, + 52.12391 + ], + [ + 20.982968, + 52.123744 + ], + [ + 20.989005, + 52.123681 + ], + [ + 20.984862, + 52.120417 + ], + [ + 20.986268, + 52.117606 + ], + [ + 20.987295, + 52.115734 + ], + [ + 20.98711, + 52.115616 + ], + [ + 20.984524, + 52.112743 + ], + [ + 20.983797, + 52.111877 + ], + [ + 20.983735, + 52.111773 + ], + [ + 20.983724, + 52.11169 + ], + [ + 20.984364, + 52.111121 + ], + [ + 20.984584, + 52.110969 + ], + [ + 20.984923, + 52.110884 + ], + [ + 20.986695, + 52.110561 + ], + [ + 20.986509, + 52.110085 + ], + [ + 20.986466, + 52.109925 + ], + [ + 20.98448, + 52.10562 + ], + [ + 20.98736, + 52.105139 + ], + [ + 20.986858, + 52.103902 + ], + [ + 20.991597, + 52.103114 + ], + [ + 20.991857, + 52.103165 + ], + [ + 20.991891, + 52.10336 + ], + [ + 20.991925, + 52.103457 + ], + [ + 20.993414, + 52.10348 + ], + [ + 20.99867, + 52.103464 + ], + [ + 20.999379, + 52.103472 + ], + [ + 21.000273, + 52.103456 + ], + [ + 21.001457, + 52.10348 + ], + [ + 21.004935, + 52.103468 + ], + [ + 21.007704, + 52.103545 + ], + [ + 21.00876, + 52.103559 + ], + [ + 21.009034, + 52.103552 + ], + [ + 21.00996, + 52.103566 + ], + [ + 21.011254, + 52.103564 + ], + [ + 21.011671, + 52.103573 + ], + [ + 21.011983, + 52.103575 + ], + [ + 21.012353, + 52.103581 + ], + [ + 21.013337, + 52.103581 + ], + [ + 21.01516, + 52.10362 + ], + [ + 21.018383, + 52.10366 + ], + [ + 21.018693, + 52.103684 + ], + [ + 21.018642, + 52.106049 + ], + [ + 21.021186, + 52.106043 + ], + [ + 21.024485, + 52.106082 + ], + [ + 21.035503, + 52.10619 + ], + [ + 21.035755, + 52.106401 + ], + [ + 21.035183, + 52.106733 + ], + [ + 21.037178, + 52.10809 + ], + [ + 21.037507, + 52.108078 + ], + [ + 21.038113, + 52.108355 + ], + [ + 21.037934, + 52.108612 + ], + [ + 21.041907, + 52.111303 + ], + [ + 21.0421, + 52.111924 + ], + [ + 21.043618, + 52.112904 + ], + [ + 21.043715, + 52.112963 + ], + [ + 21.04462, + 52.112676 + ], + [ + 21.046102, + 52.111985 + ], + [ + 21.046689, + 52.111607 + ], + [ + 21.047391, + 52.111189 + ], + [ + 21.048336, + 52.110704 + ], + [ + 21.048657, + 52.110261 + ], + [ + 21.050453, + 52.109153 + ], + [ + 21.050756, + 52.109012 + ], + [ + 21.051582, + 52.108705 + ], + [ + 21.051959, + 52.108567 + ], + [ + 21.052508, + 52.108407 + ], + [ + 21.054291, + 52.108046 + ], + [ + 21.05502, + 52.107908 + ], + [ + 21.0562, + 52.107644 + ], + [ + 21.056526, + 52.107726 + ], + [ + 21.058038, + 52.107205 + ], + [ + 21.058401, + 52.10705 + ], + [ + 21.060349, + 52.106288 + ], + [ + 21.062325, + 52.105423 + ], + [ + 21.063008, + 52.105026 + ], + [ + 21.064016, + 52.104516 + ], + [ + 21.064485, + 52.104315 + ], + [ + 21.065715, + 52.103905 + ], + [ + 21.06708, + 52.103306 + ], + [ + 21.068254, + 52.102714 + ], + [ + 21.069173, + 52.102272 + ], + [ + 21.069458, + 52.102169 + ], + [ + 21.070003, + 52.102009 + ], + [ + 21.070981, + 52.101635 + ], + [ + 21.072199, + 52.101114 + ], + [ + 21.07421, + 52.10036 + ], + [ + 21.07592, + 52.099523 + ], + [ + 21.076794, + 52.099219 + ], + [ + 21.077638, + 52.098645 + ], + [ + 21.078307, + 52.098291 + ], + [ + 21.078753, + 52.098118 + ], + [ + 21.07963, + 52.097923 + ], + [ + 21.079851, + 52.097893 + ], + [ + 21.079978, + 52.097909 + ], + [ + 21.079977, + 52.097851 + ], + [ + 21.082377, + 52.098526 + ], + [ + 21.082646, + 52.09868 + ], + [ + 21.082986, + 52.098947 + ], + [ + 21.08327, + 52.099227 + ], + [ + 21.083613, + 52.099594 + ], + [ + 21.085045, + 52.101522 + ], + [ + 21.085417, + 52.101862 + ], + [ + 21.085596, + 52.102003 + ], + [ + 21.08663, + 52.102821 + ], + [ + 21.087018, + 52.103068 + ], + [ + 21.087485, + 52.1033 + ], + [ + 21.089332, + 52.103946 + ], + [ + 21.091315, + 52.104483 + ], + [ + 21.091834, + 52.104616 + ], + [ + 21.091893, + 52.104644 + ], + [ + 21.092365, + 52.104642 + ], + [ + 21.094177, + 52.104481 + ], + [ + 21.094831, + 52.104482 + ], + [ + 21.09544, + 52.104507 + ], + [ + 21.095962, + 52.10455 + ], + [ + 21.096737, + 52.104678 + ], + [ + 21.097796, + 52.104787 + ], + [ + 21.099924, + 52.105164 + ], + [ + 21.100121, + 52.105179 + ], + [ + 21.100674, + 52.105366 + ], + [ + 21.101332, + 52.105634 + ], + [ + 21.101556, + 52.105685 + ], + [ + 21.101794, + 52.105709 + ], + [ + 21.102419, + 52.105678 + ], + [ + 21.103537, + 52.105756 + ], + [ + 21.103492, + 52.105866 + ], + [ + 21.10295, + 52.106407 + ], + [ + 21.102521, + 52.106925 + ], + [ + 21.102172, + 52.107273 + ], + [ + 21.102026, + 52.107396 + ], + [ + 21.101874, + 52.107507 + ], + [ + 21.101583, + 52.107685 + ], + [ + 21.101829, + 52.107816 + ], + [ + 21.101583, + 52.107934 + ], + [ + 21.101396, + 52.108006 + ], + [ + 21.101187, + 52.108076 + ], + [ + 21.100908, + 52.108138 + ], + [ + 21.100658, + 52.108173 + ], + [ + 21.100097, + 52.108353 + ], + [ + 21.101928, + 52.10923 + ], + [ + 21.104767, + 52.110629 + ], + [ + 21.107316, + 52.111869 + ], + [ + 21.108591, + 52.112487 + ], + [ + 21.108897, + 52.112628 + ], + [ + 21.109624, + 52.11296 + ], + [ + 21.110097, + 52.113169 + ], + [ + 21.108874, + 52.114475 + ], + [ + 21.111239, + 52.115676 + ], + [ + 21.116041, + 52.118633 + ], + [ + 21.116539, + 52.118764 + ], + [ + 21.117869, + 52.11853 + ], + [ + 21.119157, + 52.118769 + ], + [ + 21.12095, + 52.119368 + ], + [ + 21.122853, + 52.120429 + ], + [ + 21.123807, + 52.121108 + ], + [ + 21.125131, + 52.122518 + ], + [ + 21.126001, + 52.124195 + ], + [ + 21.126369, + 52.124128 + ], + [ + 21.126827, + 52.12554 + ], + [ + 21.127131, + 52.126059 + ], + [ + 21.129526, + 52.127209 + ], + [ + 21.130017, + 52.126912 + ], + [ + 21.130687, + 52.125503 + ], + [ + 21.131464, + 52.124969 + ], + [ + 21.132412, + 52.12345 + ], + [ + 21.133223, + 52.123007 + ], + [ + 21.134728, + 52.123461 + ], + [ + 21.134971, + 52.125219 + ], + [ + 21.135408, + 52.12655 + ], + [ + 21.135801, + 52.127668 + ], + [ + 21.136539, + 52.128521 + ], + [ + 21.136789, + 52.128772 + ], + [ + 21.137081, + 52.12903 + ], + [ + 21.138182, + 52.13022 + ], + [ + 21.138904, + 52.131085 + ], + [ + 21.138762, + 52.131089 + ], + [ + 21.13866, + 52.131185 + ], + [ + 21.13872, + 52.131322 + ], + [ + 21.138893, + 52.131451 + ], + [ + 21.139164, + 52.131533 + ], + [ + 21.139417, + 52.131568 + ], + [ + 21.140134, + 52.131576 + ], + [ + 21.140236, + 52.131755 + ], + [ + 21.140512, + 52.131969 + ], + [ + 21.140549, + 52.132402 + ], + [ + 21.14029, + 52.132594 + ], + [ + 21.140124, + 52.132947 + ], + [ + 21.141704, + 52.13352 + ], + [ + 21.145168, + 52.135552 + ], + [ + 21.14783, + 52.138457 + ], + [ + 21.150202, + 52.139828 + ], + [ + 21.151865, + 52.139808 + ], + [ + 21.153652, + 52.140085 + ], + [ + 21.153942, + 52.140531 + ], + [ + 21.153119, + 52.142336 + ], + [ + 21.153229, + 52.142722 + ], + [ + 21.154685, + 52.143636 + ], + [ + 21.15936, + 52.144126 + ], + [ + 21.164958, + 52.144727 + ], + [ + 21.16817, + 52.145082 + ], + [ + 21.171423, + 52.145441 + ], + [ + 21.173696, + 52.146095 + ], + [ + 21.175924, + 52.146734 + ], + [ + 21.175853, + 52.148459 + ], + [ + 21.178336, + 52.148872 + ], + [ + 21.183356, + 52.149683 + ], + [ + 21.18586, + 52.15009 + ], + [ + 21.1884, + 52.15051 + ], + [ + 21.188577, + 52.149318 + ], + [ + 21.197843, + 52.150243 + ], + [ + 21.201337, + 52.150539 + ], + [ + 21.20239, + 52.150661 + ], + [ + 21.203337, + 52.150801 + ], + [ + 21.204237, + 52.150897 + ], + [ + 21.208519, + 52.151354 + ], + [ + 21.212268, + 52.151748 + ], + [ + 21.213657, + 52.151866 + ], + [ + 21.21486, + 52.151998 + ], + [ + 21.215126, + 52.151787 + ], + [ + 21.217032, + 52.152352 + ], + [ + 21.218206, + 52.152849 + ], + [ + 21.21901, + 52.153191 + ], + [ + 21.219609, + 52.153784 + ], + [ + 21.220876, + 52.154193 + ], + [ + 21.223998, + 52.155289 + ], + [ + 21.226263, + 52.156042 + ], + [ + 21.227247, + 52.156379 + ], + [ + 21.229067, + 52.15699 + ], + [ + 21.231836, + 52.157978 + ], + [ + 21.234723, + 52.15891 + ], + [ + 21.23647, + 52.158157 + ], + [ + 21.237846, + 52.157598 + ], + [ + 21.239189, + 52.157031 + ], + [ + 21.241377, + 52.156118 + ], + [ + 21.242453, + 52.155668 + ], + [ + 21.245072, + 52.154564 + ], + [ + 21.246978, + 52.153773 + ], + [ + 21.248752, + 52.153036 + ], + [ + 21.247139, + 52.151642 + ], + [ + 21.251401, + 52.149451 + ], + [ + 21.254925, + 52.147602 + ], + [ + 21.2554, + 52.147357 + ], + [ + 21.255741, + 52.147172 + ], + [ + 21.262131, + 52.152519 + ], + [ + 21.262338, + 52.152665 + ], + [ + 21.262579, + 52.153102 + ], + [ + 21.25403, + 52.157379 + ], + [ + 21.255562, + 52.158653 + ], + [ + 21.255856, + 52.159261 + ], + [ + 21.256604, + 52.160715 + ], + [ + 21.259316, + 52.161306 + ], + [ + 21.260368, + 52.162023 + ], + [ + 21.261096, + 52.164511 + ], + [ + 21.261459, + 52.165749 + ], + [ + 21.26235, + 52.166622 + ], + [ + 21.263114, + 52.167382 + ], + [ + 21.267686, + 52.167448 + ], + [ + 21.267617, + 52.16841 + ], + [ + 21.270803, + 52.168518 + ], + [ + 21.270317, + 52.169555 + ], + [ + 21.267526, + 52.170125 + ], + [ + 21.267335, + 52.173317 + ], + [ + 21.269569, + 52.17336 + ], + [ + 21.269959, + 52.172043 + ], + [ + 21.271151, + 52.172151 + ], + [ + 21.270231, + 52.173839 + ], + [ + 21.270037, + 52.173944 + ], + [ + 21.259765, + 52.182555 + ], + [ + 21.263379, + 52.183975 + ], + [ + 21.265383, + 52.186001 + ], + [ + 21.240229, + 52.19588 + ], + [ + 21.242985, + 52.198384 + ], + [ + 21.243006, + 52.19933 + ], + [ + 21.245165, + 52.200304 + ], + [ + 21.245733, + 52.200863 + ], + [ + 21.25095, + 52.202494 + ], + [ + 21.24859, + 52.204233 + ], + [ + 21.248235, + 52.20598 + ], + [ + 21.24909, + 52.20696 + ], + [ + 21.25059, + 52.207756 + ], + [ + 21.252224, + 52.208466 + ], + [ + 21.254143, + 52.209435 + ], + [ + 21.256802, + 52.209577 + ], + [ + 21.259822, + 52.2099 + ], + [ + 21.261207, + 52.209855 + ], + [ + 21.261727, + 52.210073 + ], + [ + 21.260592, + 52.21126 + ], + [ + 21.254315, + 52.218069 + ], + [ + 21.251397, + 52.221261 + ], + [ + 21.250663, + 52.222053 + ], + [ + 21.250531, + 52.222201 + ], + [ + 21.250321, + 52.222485 + ], + [ + 21.25049, + 52.222561 + ], + [ + 21.249457, + 52.222558 + ], + [ + 21.249478, + 52.222739 + ], + [ + 21.24949, + 52.222848 + ], + [ + 21.250428, + 52.231928 + ], + [ + 21.254381, + 52.235273 + ], + [ + 21.254769, + 52.238733 + ], + [ + 21.253892, + 52.238646 + ], + [ + 21.252105, + 52.238135 + ], + [ + 21.248784, + 52.2366 + ], + [ + 21.245424, + 52.236067 + ], + [ + 21.243834, + 52.236072 + ], + [ + 21.239467, + 52.236256 + ], + [ + 21.237949, + 52.245027 + ], + [ + 21.253716, + 52.245762 + ], + [ + 21.254644, + 52.253169 + ], + [ + 21.25243, + 52.253271 + ], + [ + 21.250469, + 52.262692 + ], + [ + 21.190453, + 52.256199 + ], + [ + 21.179334, + 52.256673 + ], + [ + 21.181417, + 52.257401 + ], + [ + 21.182353, + 52.257919 + ], + [ + 21.183114, + 52.258458 + ], + [ + 21.183798, + 52.259196 + ], + [ + 21.184179, + 52.259777 + ], + [ + 21.184407, + 52.260442 + ], + [ + 21.184961, + 52.265536 + ], + [ + 21.184855, + 52.266144 + ], + [ + 21.18465, + 52.266755 + ], + [ + 21.184321, + 52.267303 + ], + [ + 21.183853, + 52.26779 + ], + [ + 21.182944, + 52.26847 + ], + [ + 21.180051, + 52.269978 + ], + [ + 21.177676, + 52.271215 + ], + [ + 21.176744, + 52.27177 + ], + [ + 21.175813, + 52.272402 + ], + [ + 21.175242, + 52.272888 + ], + [ + 21.171629, + 52.276029 + ], + [ + 21.170579, + 52.276942 + ], + [ + 21.171225, + 52.277355 + ], + [ + 21.171738, + 52.281534 + ], + [ + 21.172504, + 52.286534 + ], + [ + 21.172191, + 52.286728 + ], + [ + 21.172382, + 52.288607 + ], + [ + 21.169255, + 52.29086 + ], + [ + 21.166066, + 52.290291 + ], + [ + 21.16349, + 52.292065 + ], + [ + 21.162931, + 52.291489 + ], + [ + 21.163926, + 52.289372 + ], + [ + 21.163182, + 52.288461 + ], + [ + 21.161882, + 52.286448 + ], + [ + 21.16142, + 52.285664 + ], + [ + 21.161292, + 52.285367 + ], + [ + 21.159793, + 52.284563 + ], + [ + 21.155863, + 52.283594 + ], + [ + 21.154225, + 52.282539 + ], + [ + 21.151901, + 52.281452 + ], + [ + 21.150986, + 52.280527 + ], + [ + 21.149118, + 52.279255 + ], + [ + 21.141854, + 52.278474 + ], + [ + 21.123908, + 52.276324 + ], + [ + 21.12303, + 52.276201 + ], + [ + 21.107779, + 52.274376 + ], + [ + 21.104865, + 52.274053 + ], + [ + 21.100108, + 52.284695 + ], + [ + 21.100083, + 52.284733 + ], + [ + 21.098732, + 52.286548 + ], + [ + 21.098038, + 52.286276 + ], + [ + 21.095627, + 52.286322 + ], + [ + 21.094656, + 52.288093 + ], + [ + 21.094978, + 52.288179 + ], + [ + 21.088207, + 52.295084 + ], + [ + 21.087531, + 52.295503 + ], + [ + 21.084238, + 52.301556 + ], + [ + 21.083528, + 52.302862 + ], + [ + 21.085485, + 52.303235 + ], + [ + 21.083556, + 52.307766 + ], + [ + 21.08206, + 52.31162 + ], + [ + 21.081011, + 52.314029 + ], + [ + 21.079887, + 52.316611 + ], + [ + 21.079438, + 52.317754 + ], + [ + 21.078609, + 52.319867 + ], + [ + 21.077788, + 52.321958 + ], + [ + 21.082094, + 52.322636 + ], + [ + 21.089512, + 52.323769 + ], + [ + 21.088887, + 52.325385 + ], + [ + 21.088397, + 52.326534 + ], + [ + 21.087727, + 52.328224 + ], + [ + 21.08628, + 52.331359 + ], + [ + 21.086047, + 52.331968 + ], + [ + 21.087782, + 52.332292 + ], + [ + 21.087524, + 52.332853 + ], + [ + 21.086692, + 52.332738 + ], + [ + 21.085527, + 52.334736 + ], + [ + 21.085024, + 52.337265 + ], + [ + 21.084103, + 52.33734 + ], + [ + 21.082604, + 52.337224 + ], + [ + 21.081051, + 52.343061 + ], + [ + 21.085714, + 52.343401 + ], + [ + 21.081543, + 52.354015 + ], + [ + 21.078679, + 52.353565 + ], + [ + 21.076208, + 52.359134 + ], + [ + 21.077124, + 52.359314 + ], + [ + 21.073797, + 52.367259 + ], + [ + 21.06608, + 52.365976 + ], + [ + 21.063935, + 52.365619 + ], + [ + 21.061492, + 52.365656 + ], + [ + 21.060576, + 52.36258 + ], + [ + 21.054072, + 52.363148 + ], + [ + 21.049258, + 52.361753 + ], + [ + 21.048665, + 52.363679 + ], + [ + 21.0443, + 52.364033 + ], + [ + 21.040803, + 52.362958 + ], + [ + 21.030701, + 52.361688 + ], + [ + 21.02817, + 52.364437 + ], + [ + 21.02753, + 52.365133 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 1, + "name": "Warszawa" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.001524, + 52.228066 + ], + [ + 20.990589, + 52.22552 + ], + [ + 20.989872, + 52.225494 + ], + [ + 20.9873, + 52.224734 + ], + [ + 20.982241, + 52.223689 + ], + [ + 20.977674, + 52.222462 + ], + [ + 20.976859, + 52.222221 + ], + [ + 20.976493, + 52.221988 + ], + [ + 20.97565, + 52.222039 + ], + [ + 20.973472, + 52.221365 + ], + [ + 20.971365, + 52.22045 + ], + [ + 20.968962, + 52.219543 + ], + [ + 20.968341, + 52.220028 + ], + [ + 20.967272, + 52.219724 + ], + [ + 20.965013, + 52.219231 + ], + [ + 20.963479, + 52.218959 + ], + [ + 20.960849, + 52.21841 + ], + [ + 20.95921, + 52.218064 + ], + [ + 20.959238, + 52.217742 + ], + [ + 20.957452, + 52.216601 + ], + [ + 20.956409, + 52.215537 + ], + [ + 20.955389, + 52.214897 + ], + [ + 20.954972, + 52.214732 + ], + [ + 20.954642, + 52.214631 + ], + [ + 20.952849, + 52.214337 + ], + [ + 20.952017, + 52.214294 + ], + [ + 20.94966, + 52.21425 + ], + [ + 20.945103, + 52.214476 + ], + [ + 20.944319, + 52.214404 + ], + [ + 20.941681, + 52.213498 + ], + [ + 20.938324, + 52.212174 + ], + [ + 20.934288, + 52.211902 + ], + [ + 20.933727, + 52.211734 + ], + [ + 20.933127, + 52.211578 + ], + [ + 20.93258, + 52.211411 + ], + [ + 20.932512, + 52.211385 + ], + [ + 20.932708, + 52.211064 + ], + [ + 20.933124, + 52.210996 + ], + [ + 20.933565, + 52.210908 + ], + [ + 20.934258, + 52.210684 + ], + [ + 20.935061, + 52.210319 + ], + [ + 20.936057, + 52.209779 + ], + [ + 20.93697, + 52.209219 + ], + [ + 20.937226, + 52.209059 + ], + [ + 20.937568, + 52.208749 + ], + [ + 20.938419, + 52.207988 + ], + [ + 20.942213, + 52.204319 + ], + [ + 20.943052, + 52.203654 + ], + [ + 20.944516, + 52.202352 + ], + [ + 20.94528, + 52.201763 + ], + [ + 20.946099, + 52.201372 + ], + [ + 20.947044, + 52.201047 + ], + [ + 20.964338, + 52.197118 + ], + [ + 20.981958, + 52.192872 + ], + [ + 20.983477, + 52.197305 + ], + [ + 20.987922, + 52.210279 + ], + [ + 20.998369, + 52.209107 + ], + [ + 21.006988, + 52.210652 + ], + [ + 21.005091, + 52.216841 + ], + [ + 21.004952, + 52.217243 + ], + [ + 21.004706, + 52.223119 + ], + [ + 21.001757, + 52.227705 + ], + [ + 21.001524, + 52.228066 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 22, + "name": "Ochota" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.132421, + 52.239094 + ], + [ + 21.133133, + 52.239495 + ], + [ + 21.133459, + 52.239609 + ], + [ + 21.134108, + 52.240225 + ], + [ + 21.135659, + 52.241464 + ], + [ + 21.137425, + 52.242771 + ], + [ + 21.138191, + 52.243166 + ], + [ + 21.139446, + 52.243214 + ], + [ + 21.141764, + 52.239079 + ], + [ + 21.15327, + 52.241367 + ], + [ + 21.155556, + 52.241953 + ], + [ + 21.156435, + 52.241979 + ], + [ + 21.161711, + 52.243024 + ], + [ + 21.162226, + 52.242209 + ], + [ + 21.163202, + 52.24236 + ], + [ + 21.163613, + 52.24157 + ], + [ + 21.173388, + 52.243491 + ], + [ + 21.176528, + 52.245128 + ], + [ + 21.176921, + 52.24523 + ], + [ + 21.181216, + 52.245758 + ], + [ + 21.184018, + 52.246314 + ], + [ + 21.184598, + 52.246918 + ], + [ + 21.184083, + 52.247745 + ], + [ + 21.18347, + 52.248762 + ], + [ + 21.182993, + 52.249506 + ], + [ + 21.183926, + 52.249716 + ], + [ + 21.186152, + 52.250216 + ], + [ + 21.186281, + 52.250246 + ], + [ + 21.186877, + 52.250383 + ], + [ + 21.187007, + 52.251809 + ], + [ + 21.188772, + 52.252252 + ], + [ + 21.190213, + 52.249703 + ], + [ + 21.191212, + 52.249964 + ], + [ + 21.190453, + 52.256199 + ], + [ + 21.179334, + 52.256673 + ], + [ + 21.181417, + 52.257401 + ], + [ + 21.182353, + 52.257919 + ], + [ + 21.183114, + 52.258458 + ], + [ + 21.183798, + 52.259196 + ], + [ + 21.184179, + 52.259777 + ], + [ + 21.184407, + 52.260442 + ], + [ + 21.184961, + 52.265536 + ], + [ + 21.184855, + 52.266144 + ], + [ + 21.18465, + 52.266755 + ], + [ + 21.184321, + 52.267303 + ], + [ + 21.183853, + 52.26779 + ], + [ + 21.182944, + 52.26847 + ], + [ + 21.180051, + 52.269978 + ], + [ + 21.177676, + 52.271215 + ], + [ + 21.176744, + 52.27177 + ], + [ + 21.175813, + 52.272402 + ], + [ + 21.175242, + 52.272888 + ], + [ + 21.171629, + 52.276029 + ], + [ + 21.170579, + 52.276942 + ], + [ + 21.171225, + 52.277355 + ], + [ + 21.171738, + 52.281534 + ], + [ + 21.172504, + 52.286534 + ], + [ + 21.172191, + 52.286728 + ], + [ + 21.172382, + 52.288607 + ], + [ + 21.169255, + 52.29086 + ], + [ + 21.166066, + 52.290291 + ], + [ + 21.16349, + 52.292065 + ], + [ + 21.162931, + 52.291489 + ], + [ + 21.163926, + 52.289372 + ], + [ + 21.163182, + 52.288461 + ], + [ + 21.161882, + 52.286448 + ], + [ + 21.16142, + 52.285664 + ], + [ + 21.161292, + 52.285367 + ], + [ + 21.159793, + 52.284563 + ], + [ + 21.155863, + 52.283594 + ], + [ + 21.154225, + 52.282539 + ], + [ + 21.151901, + 52.281452 + ], + [ + 21.150986, + 52.280527 + ], + [ + 21.149118, + 52.279255 + ], + [ + 21.141854, + 52.278474 + ], + [ + 21.123908, + 52.276324 + ], + [ + 21.12303, + 52.276201 + ], + [ + 21.107779, + 52.274376 + ], + [ + 21.109493, + 52.270782 + ], + [ + 21.108573, + 52.270453 + ], + [ + 21.108863, + 52.268495 + ], + [ + 21.107618, + 52.268269 + ], + [ + 21.110501, + 52.259669 + ], + [ + 21.114629, + 52.259455 + ], + [ + 21.114587, + 52.257341 + ], + [ + 21.114469, + 52.256994 + ], + [ + 21.114321, + 52.256758 + ], + [ + 21.116416, + 52.255289 + ], + [ + 21.118888, + 52.253493 + ], + [ + 21.121809, + 52.252386 + ], + [ + 21.121964, + 52.251741 + ], + [ + 21.122345, + 52.251347 + ], + [ + 21.12276, + 52.250983 + ], + [ + 21.123247, + 52.250635 + ], + [ + 21.12416, + 52.249882 + ], + [ + 21.125858, + 52.248512 + ], + [ + 21.127175, + 52.247382 + ], + [ + 21.128701, + 52.246254 + ], + [ + 21.130396, + 52.244959 + ], + [ + 21.131339, + 52.244228 + ], + [ + 21.132341, + 52.243435 + ], + [ + 21.132542, + 52.242931 + ], + [ + 21.131825, + 52.242108 + ], + [ + 21.131544, + 52.241786 + ], + [ + 21.130396, + 52.240912 + ], + [ + 21.130197, + 52.24076 + ], + [ + 21.130937, + 52.240374 + ], + [ + 21.130756, + 52.239875 + ], + [ + 21.131645, + 52.239463 + ], + [ + 21.132421, + 52.239094 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 25, + "name": "Rembertów" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 20.873086, + 52.216665 + ], + [ + 20.873071, + 52.216546 + ], + [ + 20.873063, + 52.216489 + ], + [ + 20.873055, + 52.216444 + ], + [ + 20.87305, + 52.216415 + ], + [ + 20.872754, + 52.215232 + ], + [ + 20.87194, + 52.215264 + ], + [ + 20.87143, + 52.21316 + ], + [ + 20.871091, + 52.211178 + ], + [ + 20.871016, + 52.210382 + ], + [ + 20.870159, + 52.210391 + ], + [ + 20.870203, + 52.209857 + ], + [ + 20.870899, + 52.208853 + ], + [ + 20.876774, + 52.209392 + ], + [ + 20.878366, + 52.20933 + ], + [ + 20.878395, + 52.209057 + ], + [ + 20.880533, + 52.209002 + ], + [ + 20.881075, + 52.209412 + ], + [ + 20.88172, + 52.209677 + ], + [ + 20.885016, + 52.210332 + ], + [ + 20.887677, + 52.210793 + ], + [ + 20.89976, + 52.212348 + ], + [ + 20.914074, + 52.214079 + ], + [ + 20.91425, + 52.214527 + ], + [ + 20.914657, + 52.214584 + ], + [ + 20.915544, + 52.217218 + ], + [ + 20.915658, + 52.217243 + ], + [ + 20.915686, + 52.21733 + ], + [ + 20.918799, + 52.217769 + ], + [ + 20.919941, + 52.21791 + ], + [ + 20.920595, + 52.218087 + ], + [ + 20.921285, + 52.218335 + ], + [ + 20.922616, + 52.218963 + ], + [ + 20.923001, + 52.219227 + ], + [ + 20.923494, + 52.219586 + ], + [ + 20.923791, + 52.219897 + ], + [ + 20.924127, + 52.220303 + ], + [ + 20.924344, + 52.220648 + ], + [ + 20.924444, + 52.221019 + ], + [ + 20.924531, + 52.221392 + ], + [ + 20.924555, + 52.22163 + ], + [ + 20.92454, + 52.221872 + ], + [ + 20.924529, + 52.221936 + ], + [ + 20.924484, + 52.222271 + ], + [ + 20.924399, + 52.22228 + ], + [ + 20.924242, + 52.222502 + ], + [ + 20.923674, + 52.223647 + ], + [ + 20.923046, + 52.225024 + ], + [ + 20.922893, + 52.225353 + ], + [ + 20.922586, + 52.225926 + ], + [ + 20.922152, + 52.226715 + ], + [ + 20.921847, + 52.22728 + ], + [ + 20.921582, + 52.227847 + ], + [ + 20.921417, + 52.228456 + ], + [ + 20.921389, + 52.228846 + ], + [ + 20.921395, + 52.22922 + ], + [ + 20.92147, + 52.229392 + ], + [ + 20.921473, + 52.229412 + ], + [ + 20.921547, + 52.229761 + ], + [ + 20.921405, + 52.229749 + ], + [ + 20.921701, + 52.230874 + ], + [ + 20.922973, + 52.234995 + ], + [ + 20.924685, + 52.240274 + ], + [ + 20.924844, + 52.240296 + ], + [ + 20.924917, + 52.240354 + ], + [ + 20.925028, + 52.240794 + ], + [ + 20.924913, + 52.241193 + ], + [ + 20.92486, + 52.241241 + ], + [ + 20.924981, + 52.241347 + ], + [ + 20.925076, + 52.24133 + ], + [ + 20.925547, + 52.24239 + ], + [ + 20.926227, + 52.243356 + ], + [ + 20.926875, + 52.243903 + ], + [ + 20.927556, + 52.244354 + ], + [ + 20.938211, + 52.250225 + ], + [ + 20.940023, + 52.250714 + ], + [ + 20.942712, + 52.252153 + ], + [ + 20.944936, + 52.25361 + ], + [ + 20.947628, + 52.256501 + ], + [ + 20.947228, + 52.257094 + ], + [ + 20.950113, + 52.259608 + ], + [ + 20.951811, + 52.26136 + ], + [ + 20.953152, + 52.261518 + ], + [ + 20.953612, + 52.261851 + ], + [ + 20.95032, + 52.263106 + ], + [ + 20.94735, + 52.263879 + ], + [ + 20.943428, + 52.264983 + ], + [ + 20.943135, + 52.264675 + ], + [ + 20.942317, + 52.265034 + ], + [ + 20.941539, + 52.266256 + ], + [ + 20.939022, + 52.267078 + ], + [ + 20.936106, + 52.26772 + ], + [ + 20.930695, + 52.26872 + ], + [ + 20.925345, + 52.269926 + ], + [ + 20.919744, + 52.270965 + ], + [ + 20.916556, + 52.271683 + ], + [ + 20.915933, + 52.272032 + ], + [ + 20.901497, + 52.275443 + ], + [ + 20.893733, + 52.277173 + ], + [ + 20.892893, + 52.277494 + ], + [ + 20.890372, + 52.279151 + ], + [ + 20.889678, + 52.279832 + ], + [ + 20.88911, + 52.280578 + ], + [ + 20.888, + 52.280153 + ], + [ + 20.885985, + 52.280562 + ], + [ + 20.8854, + 52.279738 + ], + [ + 20.884793, + 52.279275 + ], + [ + 20.884311, + 52.279145 + ], + [ + 20.884017, + 52.279132 + ], + [ + 20.881175, + 52.279653 + ], + [ + 20.880149, + 52.273317 + ], + [ + 20.879683, + 52.271263 + ], + [ + 20.87887, + 52.266292 + ], + [ + 20.878783, + 52.265448 + ], + [ + 20.878689, + 52.265233 + ], + [ + 20.875555, + 52.262149 + ], + [ + 20.874951, + 52.261848 + ], + [ + 20.875101, + 52.261487 + ], + [ + 20.874838, + 52.261229 + ], + [ + 20.873237, + 52.259659 + ], + [ + 20.867329, + 52.253815 + ], + [ + 20.863919, + 52.250441 + ], + [ + 20.863679, + 52.250035 + ], + [ + 20.865665, + 52.249765 + ], + [ + 20.866581, + 52.249605 + ], + [ + 20.86699, + 52.249545 + ], + [ + 20.867732, + 52.249424 + ], + [ + 20.869345, + 52.249418 + ], + [ + 20.872265, + 52.249344 + ], + [ + 20.872712, + 52.249292 + ], + [ + 20.873736, + 52.249094 + ], + [ + 20.874492, + 52.248925 + ], + [ + 20.875834, + 52.248676 + ], + [ + 20.877057, + 52.248503 + ], + [ + 20.877833, + 52.248395 + ], + [ + 20.878808, + 52.248269 + ], + [ + 20.87994, + 52.248088 + ], + [ + 20.881002, + 52.247949 + ], + [ + 20.881963, + 52.247817 + ], + [ + 20.883502, + 52.247451 + ], + [ + 20.883635, + 52.24742 + ], + [ + 20.883875, + 52.247315 + ], + [ + 20.88463, + 52.248173 + ], + [ + 20.885027, + 52.247995 + ], + [ + 20.885011, + 52.247297 + ], + [ + 20.88763, + 52.247268 + ], + [ + 20.890771, + 52.247051 + ], + [ + 20.891077, + 52.246279 + ], + [ + 20.891242, + 52.245345 + ], + [ + 20.891385, + 52.245008 + ], + [ + 20.892076, + 52.244586 + ], + [ + 20.893152, + 52.244126 + ], + [ + 20.893843, + 52.243902 + ], + [ + 20.893778, + 52.243479 + ], + [ + 20.893752, + 52.243313 + ], + [ + 20.893623, + 52.242469 + ], + [ + 20.893016, + 52.239853 + ], + [ + 20.889546, + 52.232047 + ], + [ + 20.887277, + 52.232161 + ], + [ + 20.881921, + 52.232093 + ], + [ + 20.88068, + 52.232053 + ], + [ + 20.880398, + 52.232044 + ], + [ + 20.879732, + 52.232022 + ], + [ + 20.879547, + 52.231909 + ], + [ + 20.877977, + 52.23094 + ], + [ + 20.873101, + 52.229084 + ], + [ + 20.872903, + 52.22894 + ], + [ + 20.872786, + 52.22883 + ], + [ + 20.872712, + 52.228679 + ], + [ + 20.872689, + 52.228534 + ], + [ + 20.872725, + 52.225805 + ], + [ + 20.872682, + 52.225687 + ], + [ + 20.872619, + 52.22566 + ], + [ + 20.872537, + 52.225668 + ], + [ + 20.872699, + 52.225515 + ], + [ + 20.872891, + 52.225381 + ], + [ + 20.873099, + 52.225252 + ], + [ + 20.873205, + 52.225199 + ], + [ + 20.87344, + 52.225089 + ], + [ + 20.873657, + 52.225017 + ], + [ + 20.87406, + 52.224937 + ], + [ + 20.87528, + 52.224803 + ], + [ + 20.87463, + 52.21665 + ], + [ + 20.873086, + 52.216665 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 2, + "name": "Bemowo" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 20.996221, + 52.287339 + ], + [ + 20.997819, + 52.288013 + ], + [ + 20.997009, + 52.288704 + ], + [ + 20.99711, + 52.288955 + ], + [ + 20.997139, + 52.289198 + ], + [ + 20.997114, + 52.28943 + ], + [ + 20.998793, + 52.290338 + ], + [ + 20.998833, + 52.291733 + ], + [ + 20.998344, + 52.292136 + ], + [ + 20.99908, + 52.292822 + ], + [ + 20.998889, + 52.293022 + ], + [ + 20.9992, + 52.293159 + ], + [ + 21.000585, + 52.293303 + ], + [ + 21.001734, + 52.292197 + ], + [ + 21.00215, + 52.292057 + ], + [ + 21.002884, + 52.29133 + ], + [ + 21.004396, + 52.292021 + ], + [ + 21.004735, + 52.29162 + ], + [ + 21.005173, + 52.291643 + ], + [ + 21.005567, + 52.291701 + ], + [ + 21.006043, + 52.291819 + ], + [ + 21.009697, + 52.292989 + ], + [ + 21.009719, + 52.292953 + ], + [ + 21.010503, + 52.293238 + ], + [ + 21.01158, + 52.293578 + ], + [ + 21.011987, + 52.293757 + ], + [ + 21.012363, + 52.293979 + ], + [ + 21.012706, + 52.294299 + ], + [ + 21.012955, + 52.294599 + ], + [ + 21.013123, + 52.29485 + ], + [ + 21.013084, + 52.294931 + ], + [ + 21.012588, + 52.295723 + ], + [ + 21.013071, + 52.295852 + ], + [ + 21.01326, + 52.296641 + ], + [ + 21.013029, + 52.297496 + ], + [ + 21.014611, + 52.297936 + ], + [ + 21.015036, + 52.297383 + ], + [ + 21.016012, + 52.296616 + ], + [ + 21.017131, + 52.296914 + ], + [ + 21.017987, + 52.295902 + ], + [ + 21.020817, + 52.296747 + ], + [ + 21.023352, + 52.297785 + ], + [ + 21.022945, + 52.298454 + ], + [ + 21.023764, + 52.298838 + ], + [ + 21.024375, + 52.298215 + ], + [ + 21.031547, + 52.299834 + ], + [ + 21.031903, + 52.299931 + ], + [ + 21.032705, + 52.300097 + ], + [ + 21.033207, + 52.300209 + ], + [ + 21.034238, + 52.300512 + ], + [ + 21.036517, + 52.30102 + ], + [ + 21.037718, + 52.30127 + ], + [ + 21.03903, + 52.301573 + ], + [ + 21.040358, + 52.301906 + ], + [ + 21.040888, + 52.302057 + ], + [ + 21.041049, + 52.302042 + ], + [ + 21.041355, + 52.30213 + ], + [ + 21.041729, + 52.302236 + ], + [ + 21.041895, + 52.302281 + ], + [ + 21.042062, + 52.302325 + ], + [ + 21.042257, + 52.302377 + ], + [ + 21.042436, + 52.302416 + ], + [ + 21.044165, + 52.302853 + ], + [ + 21.045149, + 52.303102 + ], + [ + 21.046042, + 52.303337 + ], + [ + 21.047466, + 52.303695 + ], + [ + 21.048938, + 52.304127 + ], + [ + 21.050221, + 52.304577 + ], + [ + 21.051022, + 52.30479 + ], + [ + 21.051554, + 52.304918 + ], + [ + 21.051843, + 52.304944 + ], + [ + 21.051945, + 52.304913 + ], + [ + 21.057321, + 52.306281 + ], + [ + 21.060614, + 52.306995 + ], + [ + 21.061377, + 52.308298 + ], + [ + 21.062055, + 52.308394 + ], + [ + 21.062325, + 52.30741 + ], + [ + 21.065829, + 52.307817 + ], + [ + 21.068571, + 52.307885 + ], + [ + 21.083556, + 52.307766 + ], + [ + 21.08206, + 52.31162 + ], + [ + 21.081011, + 52.314029 + ], + [ + 21.079887, + 52.316611 + ], + [ + 21.079438, + 52.317754 + ], + [ + 21.078609, + 52.319867 + ], + [ + 21.077788, + 52.321958 + ], + [ + 21.082094, + 52.322636 + ], + [ + 21.089512, + 52.323769 + ], + [ + 21.088887, + 52.325385 + ], + [ + 21.088397, + 52.326534 + ], + [ + 21.087727, + 52.328224 + ], + [ + 21.08628, + 52.331359 + ], + [ + 21.086047, + 52.331968 + ], + [ + 21.087782, + 52.332292 + ], + [ + 21.087524, + 52.332853 + ], + [ + 21.086692, + 52.332738 + ], + [ + 21.085527, + 52.334736 + ], + [ + 21.085024, + 52.337265 + ], + [ + 21.084103, + 52.33734 + ], + [ + 21.082604, + 52.337224 + ], + [ + 21.081051, + 52.343061 + ], + [ + 21.085714, + 52.343401 + ], + [ + 21.081543, + 52.354015 + ], + [ + 21.078679, + 52.353565 + ], + [ + 21.076208, + 52.359134 + ], + [ + 21.077124, + 52.359314 + ], + [ + 21.073797, + 52.367259 + ], + [ + 21.06608, + 52.365976 + ], + [ + 21.063935, + 52.365619 + ], + [ + 21.061492, + 52.365656 + ], + [ + 21.060576, + 52.36258 + ], + [ + 21.054072, + 52.363148 + ], + [ + 21.049258, + 52.361753 + ], + [ + 21.048665, + 52.363679 + ], + [ + 21.0443, + 52.364033 + ], + [ + 21.040803, + 52.362958 + ], + [ + 21.030701, + 52.361688 + ], + [ + 21.02817, + 52.364437 + ], + [ + 21.02753, + 52.365133 + ], + [ + 21.02539, + 52.36337 + ], + [ + 21.024965, + 52.360891 + ], + [ + 21.024293, + 52.359968 + ], + [ + 21.023439, + 52.357431 + ], + [ + 21.02154, + 52.356737 + ], + [ + 21.020709, + 52.356961 + ], + [ + 21.013644, + 52.356036 + ], + [ + 21.013343, + 52.356826 + ], + [ + 21.00995, + 52.356796 + ], + [ + 21.008849, + 52.357134 + ], + [ + 21.008297, + 52.358211 + ], + [ + 21.008299, + 52.360131 + ], + [ + 21.008199, + 52.360158 + ], + [ + 21.005671, + 52.364096 + ], + [ + 21.004384, + 52.363847 + ], + [ + 21.003231, + 52.363649 + ], + [ + 21.002116, + 52.363581 + ], + [ + 20.99997, + 52.363276 + ], + [ + 20.99943, + 52.363155 + ], + [ + 20.998974, + 52.362878 + ], + [ + 20.998582, + 52.362656 + ], + [ + 20.997026, + 52.362289 + ], + [ + 20.996356, + 52.362089 + ], + [ + 20.99504, + 52.3619 + ], + [ + 20.994671, + 52.361823 + ], + [ + 20.991938, + 52.362769 + ], + [ + 20.981835, + 52.359978 + ], + [ + 20.978142, + 52.358973 + ], + [ + 20.971273, + 52.358976 + ], + [ + 20.970824, + 52.359853 + ], + [ + 20.966102, + 52.368153 + ], + [ + 20.954236, + 52.367857 + ], + [ + 20.953542, + 52.36776 + ], + [ + 20.95331, + 52.367659 + ], + [ + 20.95322, + 52.367597 + ], + [ + 20.952986, + 52.367261 + ], + [ + 20.952658, + 52.366524 + ], + [ + 20.952282, + 52.36645 + ], + [ + 20.951738, + 52.366492 + ], + [ + 20.950893, + 52.366451 + ], + [ + 20.950413, + 52.366416 + ], + [ + 20.949803, + 52.366397 + ], + [ + 20.948179, + 52.366455 + ], + [ + 20.944891, + 52.366697 + ], + [ + 20.94517, + 52.366218 + ], + [ + 20.943994, + 52.365972 + ], + [ + 20.943621, + 52.36636 + ], + [ + 20.940953, + 52.363773 + ], + [ + 20.93991, + 52.362491 + ], + [ + 20.93929, + 52.36177 + ], + [ + 20.935835, + 52.362973 + ], + [ + 20.935478, + 52.363 + ], + [ + 20.935088, + 52.36324 + ], + [ + 20.935859, + 52.36232 + ], + [ + 20.939195, + 52.358356 + ], + [ + 20.938873, + 52.358254 + ], + [ + 20.938499, + 52.358114 + ], + [ + 20.93592, + 52.35891 + ], + [ + 20.935542, + 52.359057 + ], + [ + 20.935104, + 52.359271 + ], + [ + 20.935033, + 52.359295 + ], + [ + 20.933973, + 52.359768 + ], + [ + 20.933093, + 52.359167 + ], + [ + 20.932901, + 52.359193 + ], + [ + 20.93167, + 52.358422 + ], + [ + 20.931419, + 52.358239 + ], + [ + 20.930927, + 52.357937 + ], + [ + 20.930136, + 52.358443 + ], + [ + 20.929269, + 52.358941 + ], + [ + 20.928368, + 52.359195 + ], + [ + 20.927491, + 52.359468 + ], + [ + 20.926863, + 52.359571 + ], + [ + 20.926343, + 52.359627 + ], + [ + 20.924082, + 52.359578 + ], + [ + 20.922972, + 52.357041 + ], + [ + 20.915136, + 52.35403 + ], + [ + 20.912431, + 52.352975 + ], + [ + 20.916528, + 52.349326 + ], + [ + 20.920064, + 52.346643 + ], + [ + 20.921461, + 52.345484 + ], + [ + 20.923335, + 52.343631 + ], + [ + 20.924969, + 52.340403 + ], + [ + 20.924989, + 52.338983 + ], + [ + 20.925398, + 52.337509 + ], + [ + 20.925778, + 52.336104 + ], + [ + 20.925641, + 52.334623 + ], + [ + 20.925123, + 52.32943 + ], + [ + 20.925016, + 52.328026 + ], + [ + 20.925209, + 52.326639 + ], + [ + 20.925731, + 52.32475 + ], + [ + 20.926579, + 52.323079 + ], + [ + 20.927782, + 52.321358 + ], + [ + 20.929422, + 52.319587 + ], + [ + 20.931033, + 52.318201 + ], + [ + 20.932617, + 52.317199 + ], + [ + 20.934719, + 52.315997 + ], + [ + 20.937339, + 52.314744 + ], + [ + 20.93974, + 52.313893 + ], + [ + 20.942087, + 52.313159 + ], + [ + 20.944106, + 52.312374 + ], + [ + 20.945716, + 52.311623 + ], + [ + 20.94749, + 52.310654 + ], + [ + 20.949673, + 52.309201 + ], + [ + 20.951338, + 52.307797 + ], + [ + 20.952513, + 52.306527 + ], + [ + 20.953932, + 52.304639 + ], + [ + 20.954532, + 52.304049 + ], + [ + 20.955369, + 52.303204 + ], + [ + 20.956092, + 52.302574 + ], + [ + 20.957632, + 52.301591 + ], + [ + 20.960388, + 52.300146 + ], + [ + 20.962609, + 52.298424 + ], + [ + 20.966678, + 52.296875 + ], + [ + 20.97055, + 52.295772 + ], + [ + 20.973986, + 52.295088 + ], + [ + 20.979111, + 52.294253 + ], + [ + 20.98211, + 52.293602 + ], + [ + 20.985409, + 52.292616 + ], + [ + 20.98789, + 52.291697 + ], + [ + 20.991171, + 52.290452 + ], + [ + 20.99354, + 52.289179 + ], + [ + 20.994781, + 52.288417 + ], + [ + 20.996221, + 52.287339 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 19, + "name": "Białołęka" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.261727, + 52.210073 + ], + [ + 21.260592, + 52.21126 + ], + [ + 21.254315, + 52.218069 + ], + [ + 21.251397, + 52.221261 + ], + [ + 21.250663, + 52.222053 + ], + [ + 21.250531, + 52.222201 + ], + [ + 21.250321, + 52.222485 + ], + [ + 21.25049, + 52.222561 + ], + [ + 21.249457, + 52.222558 + ], + [ + 21.249478, + 52.222739 + ], + [ + 21.24949, + 52.222848 + ], + [ + 21.250428, + 52.231928 + ], + [ + 21.254381, + 52.235273 + ], + [ + 21.254769, + 52.238733 + ], + [ + 21.253892, + 52.238646 + ], + [ + 21.252105, + 52.238135 + ], + [ + 21.248784, + 52.2366 + ], + [ + 21.245424, + 52.236067 + ], + [ + 21.243834, + 52.236072 + ], + [ + 21.239467, + 52.236256 + ], + [ + 21.237949, + 52.245027 + ], + [ + 21.253716, + 52.245762 + ], + [ + 21.254644, + 52.253169 + ], + [ + 21.25243, + 52.253271 + ], + [ + 21.250469, + 52.262692 + ], + [ + 21.190453, + 52.256199 + ], + [ + 21.191212, + 52.249964 + ], + [ + 21.190213, + 52.249703 + ], + [ + 21.188772, + 52.252252 + ], + [ + 21.187007, + 52.251809 + ], + [ + 21.186877, + 52.250383 + ], + [ + 21.186281, + 52.250246 + ], + [ + 21.186152, + 52.250216 + ], + [ + 21.183926, + 52.249716 + ], + [ + 21.182993, + 52.249506 + ], + [ + 21.18347, + 52.248762 + ], + [ + 21.184083, + 52.247745 + ], + [ + 21.184598, + 52.246918 + ], + [ + 21.184018, + 52.246314 + ], + [ + 21.181216, + 52.245758 + ], + [ + 21.176921, + 52.24523 + ], + [ + 21.176528, + 52.245128 + ], + [ + 21.181074, + 52.241717 + ], + [ + 21.184035, + 52.239761 + ], + [ + 21.18839, + 52.238009 + ], + [ + 21.192442, + 52.236379 + ], + [ + 21.196988, + 52.234551 + ], + [ + 21.197992, + 52.234437 + ], + [ + 21.200917, + 52.234741 + ], + [ + 21.205162, + 52.233836 + ], + [ + 21.207123, + 52.233159 + ], + [ + 21.211905, + 52.230009 + ], + [ + 21.207326, + 52.227633 + ], + [ + 21.20708, + 52.223959 + ], + [ + 21.206983, + 52.222669 + ], + [ + 21.207282, + 52.220642 + ], + [ + 21.206519, + 52.22049 + ], + [ + 21.21215, + 52.214301 + ], + [ + 21.213246, + 52.214383 + ], + [ + 21.21529, + 52.21222 + ], + [ + 21.214621, + 52.209267 + ], + [ + 21.215844, + 52.208313 + ], + [ + 21.218411, + 52.205253 + ], + [ + 21.216634, + 52.204741 + ], + [ + 21.216466, + 52.203531 + ], + [ + 21.21614, + 52.202917 + ], + [ + 21.215507, + 52.202105 + ], + [ + 21.215781, + 52.201976 + ], + [ + 21.216373, + 52.201052 + ], + [ + 21.21571, + 52.200677 + ], + [ + 21.216207, + 52.200031 + ], + [ + 21.216283, + 52.197439 + ], + [ + 21.238807, + 52.197045 + ], + [ + 21.23876, + 52.19668 + ], + [ + 21.240229, + 52.19588 + ], + [ + 21.242985, + 52.198384 + ], + [ + 21.243006, + 52.19933 + ], + [ + 21.245165, + 52.200304 + ], + [ + 21.245733, + 52.200863 + ], + [ + 21.25095, + 52.202494 + ], + [ + 21.24859, + 52.204233 + ], + [ + 21.248235, + 52.20598 + ], + [ + 21.24909, + 52.20696 + ], + [ + 21.25059, + 52.207756 + ], + [ + 21.252224, + 52.208466 + ], + [ + 21.254143, + 52.209435 + ], + [ + 21.256802, + 52.209577 + ], + [ + 21.259822, + 52.2099 + ], + [ + 21.261207, + 52.209855 + ], + [ + 21.261727, + 52.210073 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 31, + "name": "Wesoła" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 20.877148, + 52.294061 + ], + [ + 20.876347, + 52.287416 + ], + [ + 20.875787, + 52.280459 + ], + [ + 20.881175, + 52.279653 + ], + [ + 20.884017, + 52.279132 + ], + [ + 20.884311, + 52.279145 + ], + [ + 20.884793, + 52.279275 + ], + [ + 20.8854, + 52.279738 + ], + [ + 20.885985, + 52.280562 + ], + [ + 20.888, + 52.280153 + ], + [ + 20.88911, + 52.280578 + ], + [ + 20.889678, + 52.279832 + ], + [ + 20.890372, + 52.279151 + ], + [ + 20.892893, + 52.277494 + ], + [ + 20.893733, + 52.277173 + ], + [ + 20.901497, + 52.275443 + ], + [ + 20.915933, + 52.272032 + ], + [ + 20.916556, + 52.271683 + ], + [ + 20.919744, + 52.270965 + ], + [ + 20.925345, + 52.269926 + ], + [ + 20.930695, + 52.26872 + ], + [ + 20.936106, + 52.26772 + ], + [ + 20.939022, + 52.267078 + ], + [ + 20.941539, + 52.266256 + ], + [ + 20.942317, + 52.265034 + ], + [ + 20.943135, + 52.264675 + ], + [ + 20.943428, + 52.264983 + ], + [ + 20.94735, + 52.263879 + ], + [ + 20.95032, + 52.263106 + ], + [ + 20.953612, + 52.261851 + ], + [ + 20.954522, + 52.262423 + ], + [ + 20.955667, + 52.263483 + ], + [ + 20.95764, + 52.266963 + ], + [ + 20.95776, + 52.267351 + ], + [ + 20.958679, + 52.268346 + ], + [ + 20.960137, + 52.26915 + ], + [ + 20.963125, + 52.271463 + ], + [ + 20.964217, + 52.273202 + ], + [ + 20.964413, + 52.273742 + ], + [ + 20.964952, + 52.274113 + ], + [ + 20.965348, + 52.273919 + ], + [ + 20.966137, + 52.274448 + ], + [ + 20.966982, + 52.274138 + ], + [ + 20.970064, + 52.275447 + ], + [ + 20.972511, + 52.276824 + ], + [ + 20.976563, + 52.278926 + ], + [ + 20.976805, + 52.279334 + ], + [ + 20.98517, + 52.283377 + ], + [ + 20.985405, + 52.283522 + ], + [ + 20.985549, + 52.283685 + ], + [ + 20.985611, + 52.283798 + ], + [ + 20.985764, + 52.284913 + ], + [ + 20.985721, + 52.285157 + ], + [ + 20.985471, + 52.285691 + ], + [ + 20.98662, + 52.286394 + ], + [ + 20.987474, + 52.286097 + ], + [ + 20.988544, + 52.285813 + ], + [ + 20.989593, + 52.285465 + ], + [ + 20.990547, + 52.284752 + ], + [ + 20.993739, + 52.286241 + ], + [ + 20.996221, + 52.287339 + ], + [ + 20.994781, + 52.288417 + ], + [ + 20.99354, + 52.289179 + ], + [ + 20.991171, + 52.290452 + ], + [ + 20.98789, + 52.291697 + ], + [ + 20.985409, + 52.292616 + ], + [ + 20.98211, + 52.293602 + ], + [ + 20.979111, + 52.294253 + ], + [ + 20.973986, + 52.295088 + ], + [ + 20.97055, + 52.295772 + ], + [ + 20.966678, + 52.296875 + ], + [ + 20.962609, + 52.298424 + ], + [ + 20.960388, + 52.300146 + ], + [ + 20.957632, + 52.301591 + ], + [ + 20.956092, + 52.302574 + ], + [ + 20.955369, + 52.303204 + ], + [ + 20.954532, + 52.304049 + ], + [ + 20.953932, + 52.304639 + ], + [ + 20.952513, + 52.306527 + ], + [ + 20.951338, + 52.307797 + ], + [ + 20.949673, + 52.309201 + ], + [ + 20.94749, + 52.310654 + ], + [ + 20.945716, + 52.311623 + ], + [ + 20.944106, + 52.312374 + ], + [ + 20.942087, + 52.313159 + ], + [ + 20.93974, + 52.313893 + ], + [ + 20.937339, + 52.314744 + ], + [ + 20.934719, + 52.315997 + ], + [ + 20.932617, + 52.317199 + ], + [ + 20.931033, + 52.318201 + ], + [ + 20.929422, + 52.319587 + ], + [ + 20.927782, + 52.321358 + ], + [ + 20.926579, + 52.323079 + ], + [ + 20.925731, + 52.32475 + ], + [ + 20.925209, + 52.326639 + ], + [ + 20.925016, + 52.328026 + ], + [ + 20.925123, + 52.32943 + ], + [ + 20.922585, + 52.326754 + ], + [ + 20.922351, + 52.325106 + ], + [ + 20.922246, + 52.324369 + ], + [ + 20.922181, + 52.324303 + ], + [ + 20.922058, + 52.323735 + ], + [ + 20.91307, + 52.321838 + ], + [ + 20.910549, + 52.324096 + ], + [ + 20.910237, + 52.323972 + ], + [ + 20.90896, + 52.322659 + ], + [ + 20.907769, + 52.322226 + ], + [ + 20.907755, + 52.322243 + ], + [ + 20.90709, + 52.322121 + ], + [ + 20.906929, + 52.322689 + ], + [ + 20.906616, + 52.323363 + ], + [ + 20.902956, + 52.322468 + ], + [ + 20.899556, + 52.322197 + ], + [ + 20.897715, + 52.321917 + ], + [ + 20.896036, + 52.321684 + ], + [ + 20.895227, + 52.321438 + ], + [ + 20.894953, + 52.321269 + ], + [ + 20.893015, + 52.319222 + ], + [ + 20.891479, + 52.317625 + ], + [ + 20.890825, + 52.316389 + ], + [ + 20.890356, + 52.315596 + ], + [ + 20.890095, + 52.31496 + ], + [ + 20.889873, + 52.314441 + ], + [ + 20.889736, + 52.314201 + ], + [ + 20.889671, + 52.314054 + ], + [ + 20.889659, + 52.313964 + ], + [ + 20.889759, + 52.313727 + ], + [ + 20.889796, + 52.313681 + ], + [ + 20.889789, + 52.31364 + ], + [ + 20.888649, + 52.313303 + ], + [ + 20.888434, + 52.313238 + ], + [ + 20.886909, + 52.3132 + ], + [ + 20.879799, + 52.313824 + ], + [ + 20.876346, + 52.314881 + ], + [ + 20.876208, + 52.313955 + ], + [ + 20.879267, + 52.311665 + ], + [ + 20.880915, + 52.30918 + ], + [ + 20.880142, + 52.30839 + ], + [ + 20.878719, + 52.304907 + ], + [ + 20.876185, + 52.298703 + ], + [ + 20.876497, + 52.297926 + ], + [ + 20.87647, + 52.297245 + ], + [ + 20.876388, + 52.29661 + ], + [ + 20.876713, + 52.295454 + ], + [ + 20.877172, + 52.29426 + ], + [ + 20.877148, + 52.294061 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 20, + "name": "Bielany" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.030862, + 52.206856 + ], + [ + 21.030619, + 52.206831 + ], + [ + 21.030245, + 52.206815 + ], + [ + 21.029836, + 52.206842 + ], + [ + 21.029401, + 52.206933 + ], + [ + 21.028941, + 52.207056 + ], + [ + 21.028584, + 52.207211 + ], + [ + 21.025897, + 52.209264 + ], + [ + 21.025654, + 52.209526 + ], + [ + 21.02548, + 52.209766 + ], + [ + 21.025289, + 52.21013 + ], + [ + 21.024863, + 52.211022 + ], + [ + 21.024724, + 52.211215 + ], + [ + 21.024498, + 52.211434 + ], + [ + 21.022019, + 52.213123 + ], + [ + 21.02141, + 52.212942 + ], + [ + 21.02074, + 52.213316 + ], + [ + 21.018226, + 52.212814 + ], + [ + 21.017251, + 52.212467 + ], + [ + 21.01659, + 52.212301 + ], + [ + 21.015901, + 52.212178 + ], + [ + 21.009559, + 52.211027 + ], + [ + 21.007824, + 52.210754 + ], + [ + 21.006988, + 52.210652 + ], + [ + 20.998369, + 52.209107 + ], + [ + 20.987922, + 52.210279 + ], + [ + 20.983477, + 52.197305 + ], + [ + 20.981958, + 52.192872 + ], + [ + 20.983076, + 52.192602 + ], + [ + 20.984112, + 52.19217 + ], + [ + 20.98463, + 52.191756 + ], + [ + 20.985849, + 52.190691 + ], + [ + 20.986099, + 52.19034 + ], + [ + 20.986356, + 52.189859 + ], + [ + 20.986509, + 52.189189 + ], + [ + 20.986544, + 52.188519 + ], + [ + 20.986582, + 52.187198 + ], + [ + 20.98663, + 52.185511 + ], + [ + 20.986742, + 52.181572 + ], + [ + 20.986734, + 52.181452 + ], + [ + 20.986718, + 52.180401 + ], + [ + 20.986775, + 52.171133 + ], + [ + 20.987691, + 52.170582 + ], + [ + 20.988265, + 52.171464 + ], + [ + 20.996629, + 52.169522 + ], + [ + 20.997615, + 52.16946 + ], + [ + 21.003513, + 52.16809 + ], + [ + 21.007996, + 52.167057 + ], + [ + 21.008617, + 52.167882 + ], + [ + 21.009484, + 52.167422 + ], + [ + 21.011365, + 52.166939 + ], + [ + 21.012022, + 52.165473 + ], + [ + 21.015737, + 52.165495 + ], + [ + 21.015783, + 52.164794 + ], + [ + 21.016741, + 52.164799 + ], + [ + 21.01675, + 52.164698 + ], + [ + 21.01822, + 52.164215 + ], + [ + 21.019727, + 52.166123 + ], + [ + 21.019499, + 52.166803 + ], + [ + 21.020668, + 52.166584 + ], + [ + 21.021398, + 52.166471 + ], + [ + 21.024274, + 52.16646 + ], + [ + 21.024375, + 52.166802 + ], + [ + 21.025379, + 52.167363 + ], + [ + 21.025653, + 52.167526 + ], + [ + 21.025662, + 52.167661 + ], + [ + 21.027507, + 52.167649 + ], + [ + 21.027981, + 52.167576 + ], + [ + 21.028529, + 52.16743 + ], + [ + 21.029862, + 52.166716 + ], + [ + 21.030592, + 52.166784 + ], + [ + 21.035669, + 52.168135 + ], + [ + 21.037432, + 52.168752 + ], + [ + 21.038848, + 52.169487 + ], + [ + 21.039844, + 52.170346 + ], + [ + 21.040091, + 52.170727 + ], + [ + 21.042247, + 52.172372 + ], + [ + 21.043006, + 52.173354 + ], + [ + 21.043235, + 52.173865 + ], + [ + 21.043309, + 52.174937 + ], + [ + 21.045198, + 52.174504 + ], + [ + 21.04772, + 52.173153 + ], + [ + 21.051087, + 52.171355 + ], + [ + 21.053802, + 52.169404 + ], + [ + 21.054241, + 52.169457 + ], + [ + 21.054544, + 52.169466 + ], + [ + 21.055559, + 52.169574 + ], + [ + 21.057146, + 52.169713 + ], + [ + 21.057929, + 52.169778 + ], + [ + 21.058818, + 52.169921 + ], + [ + 21.059686, + 52.170151 + ], + [ + 21.059959, + 52.170192 + ], + [ + 21.061782, + 52.170218 + ], + [ + 21.063499, + 52.170092 + ], + [ + 21.065017, + 52.169807 + ], + [ + 21.065585, + 52.169731 + ], + [ + 21.066078, + 52.169709 + ], + [ + 21.067861, + 52.169676 + ], + [ + 21.068461, + 52.170244 + ], + [ + 21.066215, + 52.172337 + ], + [ + 21.064216, + 52.173961 + ], + [ + 21.064079, + 52.174084 + ], + [ + 21.064999, + 52.174431 + ], + [ + 21.066048, + 52.174811 + ], + [ + 21.065584, + 52.175291 + ], + [ + 21.065255, + 52.175568 + ], + [ + 21.06534, + 52.175596 + ], + [ + 21.065495, + 52.175674 + ], + [ + 21.0657, + 52.175382 + ], + [ + 21.06782, + 52.176051 + ], + [ + 21.068823, + 52.176376 + ], + [ + 21.0694, + 52.176704 + ], + [ + 21.069758, + 52.176979 + ], + [ + 21.070057, + 52.177393 + ], + [ + 21.07083, + 52.177603 + ], + [ + 21.072084, + 52.17836 + ], + [ + 21.072563, + 52.178736 + ], + [ + 21.072731, + 52.179267 + ], + [ + 21.081473, + 52.18452 + ], + [ + 21.089439, + 52.181208 + ], + [ + 21.089836, + 52.180557 + ], + [ + 21.095006, + 52.180898 + ], + [ + 21.094659, + 52.181725 + ], + [ + 21.094511, + 52.182337 + ], + [ + 21.094574, + 52.182751 + ], + [ + 21.095502, + 52.185369 + ], + [ + 21.096475, + 52.185935 + ], + [ + 21.097932, + 52.186079 + ], + [ + 21.099005, + 52.186629 + ], + [ + 21.099567, + 52.187333 + ], + [ + 21.100069, + 52.188864 + ], + [ + 21.099946, + 52.189454 + ], + [ + 21.099648, + 52.190029 + ], + [ + 21.099599, + 52.190358 + ], + [ + 21.099862, + 52.19097 + ], + [ + 21.100373, + 52.191368 + ], + [ + 21.101583, + 52.191957 + ], + [ + 21.102593, + 52.19217 + ], + [ + 21.103379, + 52.192499 + ], + [ + 21.103866, + 52.193172 + ], + [ + 21.10474, + 52.193738 + ], + [ + 21.105418, + 52.194102 + ], + [ + 21.104827, + 52.196623 + ], + [ + 21.104661, + 52.197703 + ], + [ + 21.104726, + 52.201525 + ], + [ + 21.104089, + 52.204073 + ], + [ + 21.103477, + 52.205564 + ], + [ + 21.102515, + 52.207362 + ], + [ + 21.100762, + 52.20969 + ], + [ + 21.099168, + 52.211223 + ], + [ + 21.095505, + 52.21394 + ], + [ + 21.0929, + 52.215315 + ], + [ + 21.092827, + 52.215356 + ], + [ + 21.090355, + 52.216333 + ], + [ + 21.087852, + 52.217123 + ], + [ + 21.08529, + 52.217733 + ], + [ + 21.08268, + 52.218214 + ], + [ + 21.080462, + 52.218497 + ], + [ + 21.078197, + 52.218652 + ], + [ + 21.075348, + 52.21873 + ], + [ + 21.072763, + 52.218617 + ], + [ + 21.070138, + 52.218529 + ], + [ + 21.066991, + 52.218666 + ], + [ + 21.063527, + 52.218938 + ], + [ + 21.061328, + 52.216611 + ], + [ + 21.061204, + 52.216558 + ], + [ + 21.054002, + 52.215386 + ], + [ + 21.054315, + 52.214718 + ], + [ + 21.049407, + 52.213956 + ], + [ + 21.049051, + 52.214699 + ], + [ + 21.04638, + 52.214256 + ], + [ + 21.04736, + 52.21137 + ], + [ + 21.045062, + 52.210174 + ], + [ + 21.04494, + 52.209858 + ], + [ + 21.044896, + 52.20981 + ], + [ + 21.044774, + 52.20973 + ], + [ + 21.044661, + 52.209688 + ], + [ + 21.042042, + 52.20964 + ], + [ + 21.039676, + 52.209636 + ], + [ + 21.036345, + 52.209576 + ], + [ + 21.03618, + 52.209464 + ], + [ + 21.034882, + 52.206862 + ], + [ + 21.032812, + 52.206996 + ], + [ + 21.032403, + 52.206991 + ], + [ + 21.031507, + 52.206921 + ], + [ + 21.030862, + 52.206856 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 21, + "name": "Mokotów" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.0929, + 52.215315 + ], + [ + 21.096721, + 52.217528 + ], + [ + 21.096963, + 52.218167 + ], + [ + 21.097473, + 52.218543 + ], + [ + 21.097814, + 52.218397 + ], + [ + 21.103592, + 52.222109 + ], + [ + 21.111992, + 52.227607 + ], + [ + 21.119478, + 52.232449 + ], + [ + 21.124675, + 52.232495 + ], + [ + 21.126252, + 52.232807 + ], + [ + 21.127387, + 52.233341 + ], + [ + 21.127761, + 52.233646 + ], + [ + 21.127898, + 52.234005 + ], + [ + 21.127953, + 52.234444 + ], + [ + 21.128443, + 52.234924 + ], + [ + 21.128367, + 52.235041 + ], + [ + 21.132421, + 52.239094 + ], + [ + 21.131645, + 52.239463 + ], + [ + 21.130756, + 52.239875 + ], + [ + 21.130937, + 52.240374 + ], + [ + 21.130197, + 52.24076 + ], + [ + 21.130396, + 52.240912 + ], + [ + 21.131544, + 52.241786 + ], + [ + 21.131825, + 52.242108 + ], + [ + 21.132542, + 52.242931 + ], + [ + 21.132341, + 52.243435 + ], + [ + 21.131339, + 52.244228 + ], + [ + 21.130396, + 52.244959 + ], + [ + 21.128701, + 52.246254 + ], + [ + 21.127175, + 52.247382 + ], + [ + 21.125858, + 52.248512 + ], + [ + 21.12416, + 52.249882 + ], + [ + 21.123247, + 52.250635 + ], + [ + 21.12276, + 52.250983 + ], + [ + 21.122345, + 52.251347 + ], + [ + 21.121964, + 52.251741 + ], + [ + 21.121809, + 52.252386 + ], + [ + 21.118888, + 52.253493 + ], + [ + 21.116416, + 52.255289 + ], + [ + 21.114321, + 52.256758 + ], + [ + 21.114469, + 52.256994 + ], + [ + 21.114587, + 52.257341 + ], + [ + 21.114629, + 52.259455 + ], + [ + 21.110501, + 52.259669 + ], + [ + 21.104294, + 52.259862 + ], + [ + 21.09913, + 52.260036 + ], + [ + 21.090307, + 52.260401 + ], + [ + 21.088276, + 52.260471 + ], + [ + 21.086415, + 52.260401 + ], + [ + 21.079328, + 52.259772 + ], + [ + 21.076567, + 52.259438 + ], + [ + 21.075814, + 52.2593 + ], + [ + 21.075887, + 52.259147 + ], + [ + 21.075627, + 52.259112 + ], + [ + 21.07421, + 52.259027 + ], + [ + 21.073264, + 52.258582 + ], + [ + 21.073013, + 52.258548 + ], + [ + 21.072538, + 52.258513 + ], + [ + 21.072534, + 52.258527 + ], + [ + 21.072068, + 52.258474 + ], + [ + 21.071209, + 52.258389 + ], + [ + 21.070446, + 52.258302 + ], + [ + 21.070213, + 52.258286 + ], + [ + 21.070033, + 52.258283 + ], + [ + 21.069811, + 52.258302 + ], + [ + 21.069545, + 52.25817 + ], + [ + 21.068966, + 52.258127 + ], + [ + 21.06811, + 52.258033 + ], + [ + 21.067763, + 52.257988 + ], + [ + 21.067238, + 52.257929 + ], + [ + 21.066783, + 52.257863 + ], + [ + 21.066643, + 52.257851 + ], + [ + 21.066036, + 52.25774 + ], + [ + 21.065623, + 52.257624 + ], + [ + 21.06523, + 52.257497 + ], + [ + 21.064767, + 52.257198 + ], + [ + 21.064457, + 52.256998 + ], + [ + 21.064164, + 52.256771 + ], + [ + 21.063965, + 52.2566 + ], + [ + 21.063784, + 52.256468 + ], + [ + 21.063462, + 52.256246 + ], + [ + 21.063175, + 52.256086 + ], + [ + 21.063161, + 52.256007 + ], + [ + 21.063094, + 52.256001 + ], + [ + 21.062631, + 52.255774 + ], + [ + 21.062202, + 52.255599 + ], + [ + 21.061655, + 52.255426 + ], + [ + 21.06118, + 52.255238 + ], + [ + 21.060253, + 52.254862 + ], + [ + 21.059105, + 52.254433 + ], + [ + 21.058777, + 52.254333 + ], + [ + 21.058197, + 52.254177 + ], + [ + 21.057825, + 52.25404 + ], + [ + 21.057219, + 52.253758 + ], + [ + 21.056032, + 52.253242 + ], + [ + 21.055114, + 52.252918 + ], + [ + 21.055069, + 52.252974 + ], + [ + 21.054963, + 52.252942 + ], + [ + 21.054979, + 52.252923 + ], + [ + 21.054861, + 52.252887 + ], + [ + 21.05314, + 52.252508 + ], + [ + 21.052759, + 52.252394 + ], + [ + 21.052805, + 52.252343 + ], + [ + 21.050737, + 52.251694 + ], + [ + 21.048727, + 52.25087 + ], + [ + 21.04856, + 52.250809 + ], + [ + 21.048543, + 52.250826 + ], + [ + 21.048456, + 52.250792 + ], + [ + 21.048471, + 52.250777 + ], + [ + 21.047982, + 52.250598 + ], + [ + 21.047726, + 52.250492 + ], + [ + 21.046453, + 52.249962 + ], + [ + 21.045748, + 52.249655 + ], + [ + 21.045434, + 52.24947 + ], + [ + 21.045212, + 52.249325 + ], + [ + 21.045097, + 52.249232 + ], + [ + 21.044887, + 52.249094 + ], + [ + 21.044874, + 52.249042 + ], + [ + 21.044909, + 52.249002 + ], + [ + 21.044712, + 52.248874 + ], + [ + 21.044543, + 52.248739 + ], + [ + 21.044361, + 52.24857 + ], + [ + 21.0442, + 52.248622 + ], + [ + 21.043968, + 52.24835 + ], + [ + 21.043745, + 52.247952 + ], + [ + 21.043909, + 52.247926 + ], + [ + 21.043845, + 52.247749 + ], + [ + 21.043802, + 52.247572 + ], + [ + 21.043563, + 52.247558 + ], + [ + 21.04346, + 52.246971 + ], + [ + 21.04345, + 52.246355 + ], + [ + 21.043461, + 52.246219 + ], + [ + 21.043396, + 52.246218 + ], + [ + 21.043452, + 52.246055 + ], + [ + 21.043436, + 52.245613 + ], + [ + 21.043409, + 52.245609 + ], + [ + 21.043399, + 52.244508 + ], + [ + 21.043358, + 52.243214 + ], + [ + 21.043714, + 52.243159 + ], + [ + 21.043644, + 52.242986 + ], + [ + 21.043291, + 52.243039 + ], + [ + 21.042876, + 52.242391 + ], + [ + 21.042344, + 52.241814 + ], + [ + 21.041736, + 52.241339 + ], + [ + 21.040848, + 52.240877 + ], + [ + 21.040909, + 52.240644 + ], + [ + 21.04053, + 52.240451 + ], + [ + 21.040478, + 52.240491 + ], + [ + 21.03685, + 52.238599 + ], + [ + 21.038525, + 52.237197 + ], + [ + 21.040109, + 52.235748 + ], + [ + 21.042333, + 52.233498 + ], + [ + 21.044381, + 52.231129 + ], + [ + 21.046494, + 52.228454 + ], + [ + 21.047678, + 52.227143 + ], + [ + 21.04894, + 52.225828 + ], + [ + 21.049372, + 52.225413 + ], + [ + 21.049894, + 52.224944 + ], + [ + 21.050802, + 52.224162 + ], + [ + 21.052209, + 52.223133 + ], + [ + 21.053674, + 52.222263 + ], + [ + 21.055754, + 52.221235 + ], + [ + 21.058364, + 52.220207 + ], + [ + 21.060945, + 52.219481 + ], + [ + 21.063527, + 52.218938 + ], + [ + 21.066991, + 52.218666 + ], + [ + 21.070138, + 52.218529 + ], + [ + 21.072763, + 52.218617 + ], + [ + 21.075348, + 52.21873 + ], + [ + 21.078197, + 52.218652 + ], + [ + 21.080462, + 52.218497 + ], + [ + 21.08268, + 52.218214 + ], + [ + 21.08529, + 52.217733 + ], + [ + 21.087852, + 52.217123 + ], + [ + 21.090355, + 52.216333 + ], + [ + 21.092827, + 52.215356 + ], + [ + 21.0929, + 52.215315 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 23, + "name": "Praga Południe" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.013123, + 52.29485 + ], + [ + 21.012955, + 52.294599 + ], + [ + 21.012706, + 52.294299 + ], + [ + 21.012363, + 52.293979 + ], + [ + 21.011987, + 52.293757 + ], + [ + 21.01158, + 52.293578 + ], + [ + 21.010503, + 52.293238 + ], + [ + 21.009719, + 52.292953 + ], + [ + 21.009697, + 52.292989 + ], + [ + 21.006043, + 52.291819 + ], + [ + 21.005567, + 52.291701 + ], + [ + 21.005173, + 52.291643 + ], + [ + 21.004735, + 52.29162 + ], + [ + 21.004396, + 52.292021 + ], + [ + 21.002884, + 52.29133 + ], + [ + 21.00215, + 52.292057 + ], + [ + 21.001734, + 52.292197 + ], + [ + 21.000585, + 52.293303 + ], + [ + 20.9992, + 52.293159 + ], + [ + 20.998889, + 52.293022 + ], + [ + 20.99908, + 52.292822 + ], + [ + 20.998344, + 52.292136 + ], + [ + 20.998833, + 52.291733 + ], + [ + 20.998793, + 52.290338 + ], + [ + 20.997114, + 52.28943 + ], + [ + 20.997139, + 52.289198 + ], + [ + 20.99711, + 52.288955 + ], + [ + 20.997009, + 52.288704 + ], + [ + 20.997819, + 52.288013 + ], + [ + 20.996221, + 52.287339 + ], + [ + 20.997423, + 52.286489 + ], + [ + 20.998451, + 52.285594 + ], + [ + 20.999503, + 52.284572 + ], + [ + 21.000347, + 52.283645 + ], + [ + 21.001018, + 52.282821 + ], + [ + 21.00151, + 52.28215 + ], + [ + 21.001963, + 52.281488 + ], + [ + 21.00252, + 52.280549 + ], + [ + 21.00312, + 52.279365 + ], + [ + 21.003546, + 52.278318 + ], + [ + 21.003885, + 52.277251 + ], + [ + 21.004096, + 52.276407 + ], + [ + 21.00426, + 52.275481 + ], + [ + 21.004361, + 52.274536 + ], + [ + 21.004437, + 52.272887 + ], + [ + 21.004578, + 52.271678 + ], + [ + 21.004728, + 52.270809 + ], + [ + 21.004924, + 52.269937 + ], + [ + 21.005142, + 52.26912 + ], + [ + 21.005401, + 52.268307 + ], + [ + 21.005715, + 52.267427 + ], + [ + 21.006264, + 52.266158 + ], + [ + 21.006895, + 52.264913 + ], + [ + 21.007767, + 52.263433 + ], + [ + 21.008591, + 52.26223 + ], + [ + 21.009374, + 52.26121 + ], + [ + 21.010317, + 52.260096 + ], + [ + 21.013968, + 52.25614 + ], + [ + 21.015727, + 52.25425 + ], + [ + 21.016405, + 52.25352 + ], + [ + 21.01748, + 52.252364 + ], + [ + 21.01855, + 52.251212 + ], + [ + 21.019676, + 52.250091 + ], + [ + 21.021021, + 52.248881 + ], + [ + 21.021875, + 52.248194 + ], + [ + 21.023129, + 52.247316 + ], + [ + 21.023789, + 52.246905 + ], + [ + 21.025237, + 52.246104 + ], + [ + 21.027171, + 52.245055 + ], + [ + 21.029301, + 52.243837 + ], + [ + 21.031244, + 52.242631 + ], + [ + 21.032866, + 52.241568 + ], + [ + 21.034376, + 52.240508 + ], + [ + 21.035631, + 52.23958 + ], + [ + 21.03685, + 52.238599 + ], + [ + 21.040478, + 52.240491 + ], + [ + 21.04053, + 52.240451 + ], + [ + 21.040909, + 52.240644 + ], + [ + 21.040848, + 52.240877 + ], + [ + 21.041736, + 52.241339 + ], + [ + 21.042344, + 52.241814 + ], + [ + 21.042876, + 52.242391 + ], + [ + 21.043291, + 52.243039 + ], + [ + 21.043644, + 52.242986 + ], + [ + 21.043714, + 52.243159 + ], + [ + 21.043358, + 52.243214 + ], + [ + 21.043399, + 52.244508 + ], + [ + 21.043409, + 52.245609 + ], + [ + 21.043436, + 52.245613 + ], + [ + 21.043452, + 52.246055 + ], + [ + 21.043396, + 52.246218 + ], + [ + 21.043461, + 52.246219 + ], + [ + 21.04345, + 52.246355 + ], + [ + 21.04346, + 52.246971 + ], + [ + 21.043563, + 52.247558 + ], + [ + 21.043802, + 52.247572 + ], + [ + 21.043845, + 52.247749 + ], + [ + 21.043909, + 52.247926 + ], + [ + 21.043745, + 52.247952 + ], + [ + 21.043968, + 52.24835 + ], + [ + 21.0442, + 52.248622 + ], + [ + 21.044361, + 52.24857 + ], + [ + 21.044543, + 52.248739 + ], + [ + 21.044712, + 52.248874 + ], + [ + 21.044909, + 52.249002 + ], + [ + 21.044874, + 52.249042 + ], + [ + 21.044887, + 52.249094 + ], + [ + 21.045097, + 52.249232 + ], + [ + 21.045212, + 52.249325 + ], + [ + 21.045434, + 52.24947 + ], + [ + 21.045748, + 52.249655 + ], + [ + 21.046453, + 52.249962 + ], + [ + 21.047726, + 52.250492 + ], + [ + 21.047982, + 52.250598 + ], + [ + 21.048471, + 52.250777 + ], + [ + 21.048456, + 52.250792 + ], + [ + 21.048543, + 52.250826 + ], + [ + 21.04856, + 52.250809 + ], + [ + 21.048727, + 52.25087 + ], + [ + 21.050737, + 52.251694 + ], + [ + 21.052805, + 52.252343 + ], + [ + 21.052759, + 52.252394 + ], + [ + 21.05314, + 52.252508 + ], + [ + 21.054861, + 52.252887 + ], + [ + 21.054979, + 52.252923 + ], + [ + 21.054963, + 52.252942 + ], + [ + 21.055069, + 52.252974 + ], + [ + 21.055114, + 52.252918 + ], + [ + 21.056032, + 52.253242 + ], + [ + 21.057219, + 52.253758 + ], + [ + 21.057825, + 52.25404 + ], + [ + 21.058197, + 52.254177 + ], + [ + 21.058777, + 52.254333 + ], + [ + 21.059105, + 52.254433 + ], + [ + 21.060253, + 52.254862 + ], + [ + 21.06118, + 52.255238 + ], + [ + 21.061655, + 52.255426 + ], + [ + 21.062202, + 52.255599 + ], + [ + 21.062631, + 52.255774 + ], + [ + 21.063094, + 52.256001 + ], + [ + 21.063161, + 52.256007 + ], + [ + 21.063175, + 52.256086 + ], + [ + 21.063462, + 52.256246 + ], + [ + 21.063784, + 52.256468 + ], + [ + 21.063965, + 52.2566 + ], + [ + 21.064164, + 52.256771 + ], + [ + 21.064457, + 52.256998 + ], + [ + 21.064767, + 52.257198 + ], + [ + 21.06523, + 52.257497 + ], + [ + 21.065623, + 52.257624 + ], + [ + 21.066036, + 52.25774 + ], + [ + 21.066643, + 52.257851 + ], + [ + 21.066783, + 52.257863 + ], + [ + 21.067238, + 52.257929 + ], + [ + 21.067763, + 52.257988 + ], + [ + 21.06811, + 52.258033 + ], + [ + 21.068966, + 52.258127 + ], + [ + 21.069545, + 52.25817 + ], + [ + 21.069811, + 52.258302 + ], + [ + 21.070033, + 52.258283 + ], + [ + 21.070213, + 52.258286 + ], + [ + 21.070446, + 52.258302 + ], + [ + 21.071209, + 52.258389 + ], + [ + 21.072068, + 52.258474 + ], + [ + 21.072534, + 52.258527 + ], + [ + 21.072538, + 52.258513 + ], + [ + 21.073013, + 52.258548 + ], + [ + 21.073264, + 52.258582 + ], + [ + 21.07421, + 52.259027 + ], + [ + 21.073972, + 52.258977 + ], + [ + 21.073381, + 52.259064 + ], + [ + 21.073002, + 52.259168 + ], + [ + 21.070733, + 52.259683 + ], + [ + 21.06746, + 52.260425 + ], + [ + 21.06427, + 52.261274 + ], + [ + 21.060166, + 52.26236 + ], + [ + 21.060085, + 52.262414 + ], + [ + 21.056748, + 52.263354 + ], + [ + 21.056415, + 52.263433 + ], + [ + 21.056027, + 52.263492 + ], + [ + 21.055571, + 52.263323 + ], + [ + 21.055156, + 52.263433 + ], + [ + 21.054695, + 52.263554 + ], + [ + 21.054814, + 52.263634 + ], + [ + 21.0535, + 52.264005 + ], + [ + 21.04951, + 52.264992 + ], + [ + 21.045557, + 52.266142 + ], + [ + 21.04394, + 52.266667 + ], + [ + 21.042534, + 52.266995 + ], + [ + 21.041209, + 52.267354 + ], + [ + 21.038766, + 52.267931 + ], + [ + 21.038654, + 52.267983 + ], + [ + 21.038583, + 52.267984 + ], + [ + 21.038549, + 52.267965 + ], + [ + 21.038529, + 52.267938 + ], + [ + 21.038272, + 52.268005 + ], + [ + 21.038289, + 52.268036 + ], + [ + 21.038286, + 52.268065 + ], + [ + 21.038221, + 52.268123 + ], + [ + 21.037969, + 52.26826 + ], + [ + 21.037886, + 52.268197 + ], + [ + 21.036882, + 52.268553 + ], + [ + 21.036603, + 52.268648 + ], + [ + 21.035613, + 52.269075 + ], + [ + 21.034689, + 52.269548 + ], + [ + 21.032726, + 52.270525 + ], + [ + 21.031937, + 52.270806 + ], + [ + 21.031427, + 52.270935 + ], + [ + 21.030918, + 52.271016 + ], + [ + 21.030314, + 52.271096 + ], + [ + 21.028475, + 52.271343 + ], + [ + 21.028242, + 52.271395 + ], + [ + 21.027522, + 52.271613 + ], + [ + 21.02712, + 52.271769 + ], + [ + 21.026571, + 52.272054 + ], + [ + 21.025762, + 52.272609 + ], + [ + 21.024742, + 52.273811 + ], + [ + 21.023135, + 52.276568 + ], + [ + 21.021132, + 52.279779 + ], + [ + 21.020823, + 52.280552 + ], + [ + 21.020701, + 52.281086 + ], + [ + 21.020642, + 52.281675 + ], + [ + 21.020723, + 52.282512 + ], + [ + 21.020836, + 52.284469 + ], + [ + 21.020765, + 52.285136 + ], + [ + 21.020578, + 52.285778 + ], + [ + 21.02071, + 52.285987 + ], + [ + 21.020684, + 52.286095 + ], + [ + 21.020423, + 52.286159 + ], + [ + 21.019886, + 52.287018 + ], + [ + 21.019043, + 52.2879 + ], + [ + 21.018482, + 52.288345 + ], + [ + 21.016851, + 52.289458 + ], + [ + 21.016229, + 52.289929 + ], + [ + 21.015637, + 52.290413 + ], + [ + 21.015049, + 52.291068 + ], + [ + 21.014589, + 52.291746 + ], + [ + 21.013845, + 52.293168 + ], + [ + 21.014029, + 52.293202 + ], + [ + 21.013887, + 52.293472 + ], + [ + 21.013755, + 52.293844 + ], + [ + 21.013123, + 52.29485 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 24, + "name": "Praga Północ" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.03685, + 52.238599 + ], + [ + 21.035631, + 52.23958 + ], + [ + 21.034376, + 52.240508 + ], + [ + 21.032866, + 52.241568 + ], + [ + 21.031244, + 52.242631 + ], + [ + 21.029301, + 52.243837 + ], + [ + 21.027171, + 52.245055 + ], + [ + 21.025237, + 52.246104 + ], + [ + 21.023789, + 52.246905 + ], + [ + 21.023129, + 52.247316 + ], + [ + 21.021875, + 52.248194 + ], + [ + 21.021021, + 52.248881 + ], + [ + 21.019676, + 52.250091 + ], + [ + 21.01855, + 52.251212 + ], + [ + 21.01748, + 52.252364 + ], + [ + 21.016405, + 52.25352 + ], + [ + 21.015727, + 52.25425 + ], + [ + 21.013968, + 52.25614 + ], + [ + 21.010317, + 52.260096 + ], + [ + 21.009374, + 52.26121 + ], + [ + 21.006616, + 52.26026 + ], + [ + 21.006542, + 52.260355 + ], + [ + 21.00592, + 52.260163 + ], + [ + 21.005837, + 52.260264 + ], + [ + 21.005742, + 52.260256 + ], + [ + 21.005619, + 52.260216 + ], + [ + 21.005373, + 52.259905 + ], + [ + 21.005383, + 52.259887 + ], + [ + 21.004657, + 52.25969 + ], + [ + 21.004677, + 52.259659 + ], + [ + 21.004002, + 52.2595 + ], + [ + 21.003892, + 52.259445 + ], + [ + 21.003534, + 52.259357 + ], + [ + 21.003437, + 52.259386 + ], + [ + 21.002565, + 52.259223 + ], + [ + 21.00052, + 52.259089 + ], + [ + 21.000116, + 52.259101 + ], + [ + 21.000107, + 52.259055 + ], + [ + 20.999637, + 52.259073 + ], + [ + 20.999552, + 52.259092 + ], + [ + 20.99917, + 52.259395 + ], + [ + 20.997471, + 52.25943 + ], + [ + 20.997466, + 52.259335 + ], + [ + 20.995148, + 52.259388 + ], + [ + 20.995023, + 52.259391 + ], + [ + 20.99482, + 52.259445 + ], + [ + 20.994644, + 52.259399 + ], + [ + 20.990218, + 52.259491 + ], + [ + 20.990219, + 52.259542 + ], + [ + 20.989834, + 52.25955 + ], + [ + 20.989263, + 52.259548 + ], + [ + 20.989118, + 52.259552 + ], + [ + 20.988996, + 52.259551 + ], + [ + 20.988838, + 52.259543 + ], + [ + 20.988815, + 52.259587 + ], + [ + 20.98763, + 52.259604 + ], + [ + 20.986619, + 52.259638 + ], + [ + 20.986628, + 52.259763 + ], + [ + 20.985476, + 52.259773 + ], + [ + 20.984931, + 52.259749 + ], + [ + 20.984309, + 52.259683 + ], + [ + 20.983544, + 52.259653 + ], + [ + 20.982843, + 52.259592 + ], + [ + 20.981605, + 52.259601 + ], + [ + 20.981027, + 52.259436 + ], + [ + 20.981389, + 52.257947 + ], + [ + 20.981657, + 52.256957 + ], + [ + 20.982508, + 52.254803 + ], + [ + 20.982644, + 52.25457 + ], + [ + 20.983674, + 52.253386 + ], + [ + 20.984306, + 52.25266 + ], + [ + 20.989263, + 52.246883 + ], + [ + 20.993604, + 52.241886 + ], + [ + 20.993848, + 52.241622 + ], + [ + 20.995212, + 52.239978 + ], + [ + 20.995423, + 52.239693 + ], + [ + 20.995606, + 52.239382 + ], + [ + 20.996998, + 52.236016 + ], + [ + 20.997152, + 52.235649 + ], + [ + 20.998179, + 52.233211 + ], + [ + 21.000006, + 52.230527 + ], + [ + 21.001524, + 52.228066 + ], + [ + 21.001757, + 52.227705 + ], + [ + 21.004706, + 52.223119 + ], + [ + 21.004952, + 52.217243 + ], + [ + 21.005091, + 52.216841 + ], + [ + 21.006988, + 52.210652 + ], + [ + 21.007824, + 52.210754 + ], + [ + 21.009559, + 52.211027 + ], + [ + 21.015901, + 52.212178 + ], + [ + 21.01659, + 52.212301 + ], + [ + 21.017251, + 52.212467 + ], + [ + 21.018226, + 52.212814 + ], + [ + 21.02074, + 52.213316 + ], + [ + 21.02141, + 52.212942 + ], + [ + 21.022019, + 52.213123 + ], + [ + 21.024498, + 52.211434 + ], + [ + 21.024724, + 52.211215 + ], + [ + 21.024863, + 52.211022 + ], + [ + 21.025289, + 52.21013 + ], + [ + 21.02548, + 52.209766 + ], + [ + 21.025654, + 52.209526 + ], + [ + 21.025897, + 52.209264 + ], + [ + 21.028584, + 52.207211 + ], + [ + 21.028941, + 52.207056 + ], + [ + 21.029401, + 52.206933 + ], + [ + 21.029836, + 52.206842 + ], + [ + 21.030245, + 52.206815 + ], + [ + 21.030619, + 52.206831 + ], + [ + 21.030862, + 52.206856 + ], + [ + 21.031507, + 52.206921 + ], + [ + 21.032403, + 52.206991 + ], + [ + 21.032812, + 52.206996 + ], + [ + 21.034882, + 52.206862 + ], + [ + 21.03618, + 52.209464 + ], + [ + 21.036345, + 52.209576 + ], + [ + 21.039676, + 52.209636 + ], + [ + 21.042042, + 52.20964 + ], + [ + 21.044661, + 52.209688 + ], + [ + 21.044774, + 52.20973 + ], + [ + 21.044896, + 52.20981 + ], + [ + 21.04494, + 52.209858 + ], + [ + 21.045062, + 52.210174 + ], + [ + 21.04736, + 52.21137 + ], + [ + 21.04638, + 52.214256 + ], + [ + 21.049051, + 52.214699 + ], + [ + 21.049407, + 52.213956 + ], + [ + 21.054315, + 52.214718 + ], + [ + 21.054002, + 52.215386 + ], + [ + 21.061204, + 52.216558 + ], + [ + 21.061328, + 52.216611 + ], + [ + 21.063527, + 52.218938 + ], + [ + 21.060945, + 52.219481 + ], + [ + 21.058364, + 52.220207 + ], + [ + 21.055754, + 52.221235 + ], + [ + 21.053674, + 52.222263 + ], + [ + 21.052209, + 52.223133 + ], + [ + 21.050802, + 52.224162 + ], + [ + 21.049894, + 52.224944 + ], + [ + 21.049372, + 52.225413 + ], + [ + 21.04894, + 52.225828 + ], + [ + 21.047678, + 52.227143 + ], + [ + 21.046494, + 52.228454 + ], + [ + 21.044381, + 52.231129 + ], + [ + 21.042333, + 52.233498 + ], + [ + 21.040109, + 52.235748 + ], + [ + 21.038525, + 52.237197 + ], + [ + 21.03685, + 52.238599 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 26, + "name": "Śródmieście" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.036882, + 52.268553 + ], + [ + 21.037886, + 52.268197 + ], + [ + 21.037969, + 52.26826 + ], + [ + 21.038221, + 52.268123 + ], + [ + 21.038286, + 52.268065 + ], + [ + 21.038289, + 52.268036 + ], + [ + 21.038272, + 52.268005 + ], + [ + 21.038529, + 52.267938 + ], + [ + 21.038549, + 52.267965 + ], + [ + 21.038583, + 52.267984 + ], + [ + 21.038654, + 52.267983 + ], + [ + 21.038766, + 52.267931 + ], + [ + 21.041209, + 52.267354 + ], + [ + 21.042534, + 52.266995 + ], + [ + 21.04394, + 52.266667 + ], + [ + 21.045557, + 52.266142 + ], + [ + 21.04951, + 52.264992 + ], + [ + 21.0535, + 52.264005 + ], + [ + 21.054814, + 52.263634 + ], + [ + 21.054695, + 52.263554 + ], + [ + 21.055156, + 52.263433 + ], + [ + 21.055571, + 52.263323 + ], + [ + 21.056027, + 52.263492 + ], + [ + 21.056415, + 52.263433 + ], + [ + 21.056748, + 52.263354 + ], + [ + 21.060085, + 52.262414 + ], + [ + 21.060166, + 52.26236 + ], + [ + 21.06427, + 52.261274 + ], + [ + 21.06746, + 52.260425 + ], + [ + 21.070733, + 52.259683 + ], + [ + 21.073002, + 52.259168 + ], + [ + 21.073381, + 52.259064 + ], + [ + 21.073972, + 52.258977 + ], + [ + 21.07421, + 52.259027 + ], + [ + 21.075627, + 52.259112 + ], + [ + 21.075887, + 52.259147 + ], + [ + 21.075814, + 52.2593 + ], + [ + 21.076567, + 52.259438 + ], + [ + 21.079328, + 52.259772 + ], + [ + 21.086415, + 52.260401 + ], + [ + 21.088276, + 52.260471 + ], + [ + 21.090307, + 52.260401 + ], + [ + 21.09913, + 52.260036 + ], + [ + 21.104294, + 52.259862 + ], + [ + 21.110501, + 52.259669 + ], + [ + 21.107618, + 52.268269 + ], + [ + 21.108863, + 52.268495 + ], + [ + 21.108573, + 52.270453 + ], + [ + 21.109493, + 52.270782 + ], + [ + 21.107779, + 52.274376 + ], + [ + 21.104865, + 52.274053 + ], + [ + 21.100108, + 52.284695 + ], + [ + 21.100083, + 52.284733 + ], + [ + 21.098732, + 52.286548 + ], + [ + 21.098038, + 52.286276 + ], + [ + 21.095627, + 52.286322 + ], + [ + 21.094656, + 52.288093 + ], + [ + 21.094978, + 52.288179 + ], + [ + 21.088207, + 52.295084 + ], + [ + 21.087531, + 52.295503 + ], + [ + 21.084238, + 52.301556 + ], + [ + 21.083528, + 52.302862 + ], + [ + 21.085485, + 52.303235 + ], + [ + 21.083556, + 52.307766 + ], + [ + 21.068571, + 52.307885 + ], + [ + 21.065829, + 52.307817 + ], + [ + 21.062325, + 52.30741 + ], + [ + 21.062055, + 52.308394 + ], + [ + 21.061377, + 52.308298 + ], + [ + 21.060614, + 52.306995 + ], + [ + 21.057321, + 52.306281 + ], + [ + 21.051945, + 52.304913 + ], + [ + 21.051843, + 52.304944 + ], + [ + 21.051554, + 52.304918 + ], + [ + 21.051022, + 52.30479 + ], + [ + 21.050221, + 52.304577 + ], + [ + 21.048938, + 52.304127 + ], + [ + 21.047466, + 52.303695 + ], + [ + 21.046042, + 52.303337 + ], + [ + 21.045149, + 52.303102 + ], + [ + 21.044165, + 52.302853 + ], + [ + 21.042436, + 52.302416 + ], + [ + 21.042257, + 52.302377 + ], + [ + 21.042062, + 52.302325 + ], + [ + 21.041895, + 52.302281 + ], + [ + 21.041729, + 52.302236 + ], + [ + 21.041355, + 52.30213 + ], + [ + 21.041049, + 52.302042 + ], + [ + 21.040888, + 52.302057 + ], + [ + 21.040358, + 52.301906 + ], + [ + 21.03903, + 52.301573 + ], + [ + 21.037718, + 52.30127 + ], + [ + 21.036517, + 52.30102 + ], + [ + 21.034238, + 52.300512 + ], + [ + 21.033207, + 52.300209 + ], + [ + 21.032705, + 52.300097 + ], + [ + 21.031903, + 52.299931 + ], + [ + 21.031547, + 52.299834 + ], + [ + 21.024375, + 52.298215 + ], + [ + 21.023764, + 52.298838 + ], + [ + 21.022945, + 52.298454 + ], + [ + 21.023352, + 52.297785 + ], + [ + 21.020817, + 52.296747 + ], + [ + 21.017987, + 52.295902 + ], + [ + 21.017131, + 52.296914 + ], + [ + 21.016012, + 52.296616 + ], + [ + 21.015036, + 52.297383 + ], + [ + 21.014611, + 52.297936 + ], + [ + 21.013029, + 52.297496 + ], + [ + 21.01326, + 52.296641 + ], + [ + 21.013071, + 52.295852 + ], + [ + 21.012588, + 52.295723 + ], + [ + 21.013084, + 52.294931 + ], + [ + 21.013123, + 52.29485 + ], + [ + 21.013755, + 52.293844 + ], + [ + 21.013887, + 52.293472 + ], + [ + 21.014029, + 52.293202 + ], + [ + 21.013845, + 52.293168 + ], + [ + 21.014589, + 52.291746 + ], + [ + 21.015049, + 52.291068 + ], + [ + 21.015637, + 52.290413 + ], + [ + 21.016229, + 52.289929 + ], + [ + 21.016851, + 52.289458 + ], + [ + 21.018482, + 52.288345 + ], + [ + 21.019043, + 52.2879 + ], + [ + 21.019886, + 52.287018 + ], + [ + 21.020423, + 52.286159 + ], + [ + 21.020684, + 52.286095 + ], + [ + 21.02071, + 52.285987 + ], + [ + 21.020578, + 52.285778 + ], + [ + 21.020765, + 52.285136 + ], + [ + 21.020836, + 52.284469 + ], + [ + 21.020723, + 52.282512 + ], + [ + 21.020642, + 52.281675 + ], + [ + 21.020701, + 52.281086 + ], + [ + 21.020823, + 52.280552 + ], + [ + 21.021132, + 52.279779 + ], + [ + 21.023135, + 52.276568 + ], + [ + 21.024742, + 52.273811 + ], + [ + 21.025762, + 52.272609 + ], + [ + 21.026571, + 52.272054 + ], + [ + 21.02712, + 52.271769 + ], + [ + 21.027522, + 52.271613 + ], + [ + 21.028242, + 52.271395 + ], + [ + 21.028475, + 52.271343 + ], + [ + 21.030314, + 52.271096 + ], + [ + 21.030918, + 52.271016 + ], + [ + 21.031427, + 52.270935 + ], + [ + 21.031937, + 52.270806 + ], + [ + 21.032726, + 52.270525 + ], + [ + 21.034689, + 52.269548 + ], + [ + 21.035613, + 52.269075 + ], + [ + 21.036603, + 52.268648 + ], + [ + 21.036882, + 52.268553 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 27, + "name": "Targówek" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 20.851917, + 52.199717 + ], + [ + 20.852018, + 52.199438 + ], + [ + 20.852069, + 52.199319 + ], + [ + 20.852723, + 52.197859 + ], + [ + 20.853044, + 52.197258 + ], + [ + 20.853303, + 52.197266 + ], + [ + 20.853306, + 52.197236 + ], + [ + 20.853065, + 52.197195 + ], + [ + 20.853819, + 52.195261 + ], + [ + 20.854239, + 52.194568 + ], + [ + 20.854647, + 52.193975 + ], + [ + 20.856199, + 52.19204 + ], + [ + 20.856685, + 52.191299 + ], + [ + 20.856905, + 52.190439 + ], + [ + 20.856767, + 52.189131 + ], + [ + 20.856445, + 52.188537 + ], + [ + 20.856057, + 52.187973 + ], + [ + 20.855934, + 52.18787 + ], + [ + 20.856034, + 52.187702 + ], + [ + 20.861457, + 52.189316 + ], + [ + 20.861664, + 52.189232 + ], + [ + 20.86335, + 52.18963 + ], + [ + 20.867093, + 52.190765 + ], + [ + 20.867202, + 52.182496 + ], + [ + 20.869159, + 52.18259 + ], + [ + 20.869487, + 52.18259 + ], + [ + 20.870628, + 52.182495 + ], + [ + 20.871509, + 52.182487 + ], + [ + 20.872605, + 52.182355 + ], + [ + 20.874522, + 52.182339 + ], + [ + 20.874813, + 52.182362 + ], + [ + 20.878698, + 52.182522 + ], + [ + 20.879765, + 52.18255 + ], + [ + 20.880261, + 52.182528 + ], + [ + 20.881145, + 52.182488 + ], + [ + 20.881958, + 52.182479 + ], + [ + 20.88565, + 52.182552 + ], + [ + 20.892337, + 52.182525 + ], + [ + 20.897733, + 52.182521 + ], + [ + 20.899275, + 52.184131 + ], + [ + 20.899754, + 52.184201 + ], + [ + 20.901475, + 52.18442 + ], + [ + 20.90159, + 52.184417 + ], + [ + 20.902087, + 52.184251 + ], + [ + 20.902186, + 52.184173 + ], + [ + 20.90266, + 52.18402 + ], + [ + 20.902706, + 52.184068 + ], + [ + 20.903928, + 52.18397 + ], + [ + 20.903971, + 52.184114 + ], + [ + 20.904079, + 52.184118 + ], + [ + 20.909038, + 52.184544 + ], + [ + 20.909088, + 52.184938 + ], + [ + 20.909157, + 52.185296 + ], + [ + 20.909178, + 52.185627 + ], + [ + 20.909209, + 52.185858 + ], + [ + 20.91045, + 52.185813 + ], + [ + 20.910853, + 52.18579 + ], + [ + 20.911347, + 52.18578 + ], + [ + 20.913873, + 52.185851 + ], + [ + 20.914769, + 52.185843 + ], + [ + 20.916568, + 52.188818 + ], + [ + 20.916821, + 52.189488 + ], + [ + 20.917459, + 52.190635 + ], + [ + 20.9192, + 52.193762 + ], + [ + 20.91577, + 52.192751 + ], + [ + 20.915382, + 52.192733 + ], + [ + 20.913217, + 52.193721 + ], + [ + 20.912126, + 52.19428 + ], + [ + 20.910453, + 52.1957 + ], + [ + 20.910542, + 52.195975 + ], + [ + 20.908182, + 52.196789 + ], + [ + 20.9063, + 52.197604 + ], + [ + 20.904509, + 52.198043 + ], + [ + 20.90318, + 52.198326 + ], + [ + 20.902254, + 52.198683 + ], + [ + 20.901612, + 52.198994 + ], + [ + 20.900674, + 52.199519 + ], + [ + 20.899968, + 52.200047 + ], + [ + 20.900143, + 52.201707 + ], + [ + 20.898949, + 52.202027 + ], + [ + 20.897948, + 52.202301 + ], + [ + 20.896723, + 52.202722 + ], + [ + 20.893452, + 52.204057 + ], + [ + 20.88795, + 52.206256 + ], + [ + 20.881168, + 52.208946 + ], + [ + 20.880533, + 52.209002 + ], + [ + 20.878395, + 52.209057 + ], + [ + 20.878366, + 52.20933 + ], + [ + 20.876774, + 52.209392 + ], + [ + 20.870899, + 52.208853 + ], + [ + 20.870584, + 52.20884 + ], + [ + 20.870566, + 52.208844 + ], + [ + 20.869921, + 52.208761 + ], + [ + 20.868649, + 52.208627 + ], + [ + 20.865805, + 52.208307 + ], + [ + 20.864618, + 52.208151 + ], + [ + 20.864652, + 52.208057 + ], + [ + 20.862685, + 52.20782 + ], + [ + 20.86267, + 52.207867 + ], + [ + 20.862654, + 52.207913 + ], + [ + 20.861089, + 52.207721 + ], + [ + 20.859603, + 52.20745 + ], + [ + 20.858724, + 52.207263 + ], + [ + 20.858565, + 52.207211 + ], + [ + 20.858076, + 52.207098 + ], + [ + 20.85743, + 52.206907 + ], + [ + 20.856791, + 52.206681 + ], + [ + 20.856158, + 52.206423 + ], + [ + 20.855591, + 52.206144 + ], + [ + 20.855079, + 52.205865 + ], + [ + 20.854554, + 52.205539 + ], + [ + 20.854112, + 52.205224 + ], + [ + 20.853631, + 52.204835 + ], + [ + 20.853221, + 52.204456 + ], + [ + 20.852917, + 52.204101 + ], + [ + 20.852897, + 52.20406 + ], + [ + 20.853319, + 52.204079 + ], + [ + 20.853335, + 52.203993 + ], + [ + 20.852765, + 52.203925 + ], + [ + 20.852591, + 52.203691 + ], + [ + 20.852324, + 52.203289 + ], + [ + 20.852103, + 52.202863 + ], + [ + 20.851986, + 52.202352 + ], + [ + 20.851911, + 52.202019 + ], + [ + 20.851869, + 52.201827 + ], + [ + 20.851856, + 52.201762 + ], + [ + 20.851688, + 52.200977 + ], + [ + 20.851699, + 52.200838 + ], + [ + 20.851709, + 52.200723 + ], + [ + 20.851716, + 52.200681 + ], + [ + 20.851769, + 52.200276 + ], + [ + 20.851882, + 52.199832 + ], + [ + 20.852163, + 52.199792 + ], + [ + 20.852158, + 52.199721 + ], + [ + 20.851917, + 52.199717 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 28, + "name": "Ursus" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.001524, + 52.228066 + ], + [ + 21.000006, + 52.230527 + ], + [ + 20.998179, + 52.233211 + ], + [ + 20.997152, + 52.235649 + ], + [ + 20.996998, + 52.236016 + ], + [ + 20.995606, + 52.239382 + ], + [ + 20.995423, + 52.239693 + ], + [ + 20.995212, + 52.239978 + ], + [ + 20.993848, + 52.241622 + ], + [ + 20.993604, + 52.241886 + ], + [ + 20.989263, + 52.246883 + ], + [ + 20.984306, + 52.25266 + ], + [ + 20.983674, + 52.253386 + ], + [ + 20.982644, + 52.25457 + ], + [ + 20.982508, + 52.254803 + ], + [ + 20.981657, + 52.256957 + ], + [ + 20.981389, + 52.257947 + ], + [ + 20.981027, + 52.259436 + ], + [ + 20.979573, + 52.25902 + ], + [ + 20.979457, + 52.259143 + ], + [ + 20.979434, + 52.259135 + ], + [ + 20.979429, + 52.259138 + ], + [ + 20.973562, + 52.257068 + ], + [ + 20.973648, + 52.256959 + ], + [ + 20.972644, + 52.25658 + ], + [ + 20.971095, + 52.256042 + ], + [ + 20.970489, + 52.255794 + ], + [ + 20.970473, + 52.255716 + ], + [ + 20.970141, + 52.255601 + ], + [ + 20.970015, + 52.255585 + ], + [ + 20.969723, + 52.255731 + ], + [ + 20.966406, + 52.254574 + ], + [ + 20.965589, + 52.254311 + ], + [ + 20.96547, + 52.254303 + ], + [ + 20.964516, + 52.254378 + ], + [ + 20.963291, + 52.254988 + ], + [ + 20.963237, + 52.255006 + ], + [ + 20.963186, + 52.255021 + ], + [ + 20.96306, + 52.255027 + ], + [ + 20.962366, + 52.255085 + ], + [ + 20.96195, + 52.255176 + ], + [ + 20.960023, + 52.25583 + ], + [ + 20.95855, + 52.255786 + ], + [ + 20.956273, + 52.255468 + ], + [ + 20.954667, + 52.255261 + ], + [ + 20.95422, + 52.255386 + ], + [ + 20.95155, + 52.255532 + ], + [ + 20.950905, + 52.255662 + ], + [ + 20.95024, + 52.255874 + ], + [ + 20.948005, + 52.256772 + ], + [ + 20.947628, + 52.256501 + ], + [ + 20.944936, + 52.25361 + ], + [ + 20.942712, + 52.252153 + ], + [ + 20.940023, + 52.250714 + ], + [ + 20.938211, + 52.250225 + ], + [ + 20.927556, + 52.244354 + ], + [ + 20.926875, + 52.243903 + ], + [ + 20.926227, + 52.243356 + ], + [ + 20.925547, + 52.24239 + ], + [ + 20.925076, + 52.24133 + ], + [ + 20.924981, + 52.241347 + ], + [ + 20.92486, + 52.241241 + ], + [ + 20.924913, + 52.241193 + ], + [ + 20.925028, + 52.240794 + ], + [ + 20.924917, + 52.240354 + ], + [ + 20.924844, + 52.240296 + ], + [ + 20.924685, + 52.240274 + ], + [ + 20.922973, + 52.234995 + ], + [ + 20.921701, + 52.230874 + ], + [ + 20.921405, + 52.229749 + ], + [ + 20.921547, + 52.229761 + ], + [ + 20.921473, + 52.229412 + ], + [ + 20.92147, + 52.229392 + ], + [ + 20.921395, + 52.22922 + ], + [ + 20.921389, + 52.228846 + ], + [ + 20.921417, + 52.228456 + ], + [ + 20.921582, + 52.227847 + ], + [ + 20.921847, + 52.22728 + ], + [ + 20.922152, + 52.226715 + ], + [ + 20.922586, + 52.225926 + ], + [ + 20.922893, + 52.225353 + ], + [ + 20.923046, + 52.225024 + ], + [ + 20.923674, + 52.223647 + ], + [ + 20.924242, + 52.222502 + ], + [ + 20.924399, + 52.22228 + ], + [ + 20.924484, + 52.222271 + ], + [ + 20.924529, + 52.221936 + ], + [ + 20.92454, + 52.221872 + ], + [ + 20.924555, + 52.22163 + ], + [ + 20.924531, + 52.221392 + ], + [ + 20.924444, + 52.221019 + ], + [ + 20.924344, + 52.220648 + ], + [ + 20.924127, + 52.220303 + ], + [ + 20.923791, + 52.219897 + ], + [ + 20.923494, + 52.219586 + ], + [ + 20.923001, + 52.219227 + ], + [ + 20.922616, + 52.218963 + ], + [ + 20.921285, + 52.218335 + ], + [ + 20.920595, + 52.218087 + ], + [ + 20.919941, + 52.21791 + ], + [ + 20.918799, + 52.217769 + ], + [ + 20.915686, + 52.21733 + ], + [ + 20.915658, + 52.217243 + ], + [ + 20.915544, + 52.217218 + ], + [ + 20.914657, + 52.214584 + ], + [ + 20.916227, + 52.214391 + ], + [ + 20.91832, + 52.214555 + ], + [ + 20.921612, + 52.21354 + ], + [ + 20.923797, + 52.21375 + ], + [ + 20.924332, + 52.213504 + ], + [ + 20.924393, + 52.213385 + ], + [ + 20.923799, + 52.213208 + ], + [ + 20.923311, + 52.212949 + ], + [ + 20.922876, + 52.212634 + ], + [ + 20.922569, + 52.21211 + ], + [ + 20.922512, + 52.211688 + ], + [ + 20.922616, + 52.211253 + ], + [ + 20.922863, + 52.210869 + ], + [ + 20.923336, + 52.210472 + ], + [ + 20.923982, + 52.210144 + ], + [ + 20.924927, + 52.20995 + ], + [ + 20.925569, + 52.209912 + ], + [ + 20.925594, + 52.209477 + ], + [ + 20.926754, + 52.209535 + ], + [ + 20.927656, + 52.2098 + ], + [ + 20.930528, + 52.210791 + ], + [ + 20.932202, + 52.211335 + ], + [ + 20.932512, + 52.211385 + ], + [ + 20.93258, + 52.211411 + ], + [ + 20.933127, + 52.211578 + ], + [ + 20.933727, + 52.211734 + ], + [ + 20.934288, + 52.211902 + ], + [ + 20.938324, + 52.212174 + ], + [ + 20.941681, + 52.213498 + ], + [ + 20.944319, + 52.214404 + ], + [ + 20.945103, + 52.214476 + ], + [ + 20.94966, + 52.21425 + ], + [ + 20.952017, + 52.214294 + ], + [ + 20.952849, + 52.214337 + ], + [ + 20.954642, + 52.214631 + ], + [ + 20.954972, + 52.214732 + ], + [ + 20.955389, + 52.214897 + ], + [ + 20.956409, + 52.215537 + ], + [ + 20.957452, + 52.216601 + ], + [ + 20.959238, + 52.217742 + ], + [ + 20.95921, + 52.218064 + ], + [ + 20.960849, + 52.21841 + ], + [ + 20.963479, + 52.218959 + ], + [ + 20.965013, + 52.219231 + ], + [ + 20.967272, + 52.219724 + ], + [ + 20.968341, + 52.220028 + ], + [ + 20.968962, + 52.219543 + ], + [ + 20.971365, + 52.22045 + ], + [ + 20.973472, + 52.221365 + ], + [ + 20.97565, + 52.222039 + ], + [ + 20.976493, + 52.221988 + ], + [ + 20.976859, + 52.222221 + ], + [ + 20.977674, + 52.222462 + ], + [ + 20.982241, + 52.223689 + ], + [ + 20.9873, + 52.224734 + ], + [ + 20.989872, + 52.225494 + ], + [ + 20.990589, + 52.22552 + ], + [ + 21.001524, + 52.228066 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 34, + "name": "Wola" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 20.979608, + 52.129474 + ], + [ + 20.979608, + 52.127582 + ], + [ + 20.98252, + 52.12391 + ], + [ + 20.982968, + 52.123744 + ], + [ + 20.989005, + 52.123681 + ], + [ + 20.984862, + 52.120417 + ], + [ + 20.986268, + 52.117606 + ], + [ + 20.987295, + 52.115734 + ], + [ + 20.98711, + 52.115616 + ], + [ + 20.984524, + 52.112743 + ], + [ + 20.983797, + 52.111877 + ], + [ + 20.983735, + 52.111773 + ], + [ + 20.983724, + 52.11169 + ], + [ + 20.984364, + 52.111121 + ], + [ + 20.984584, + 52.110969 + ], + [ + 20.984923, + 52.110884 + ], + [ + 20.986695, + 52.110561 + ], + [ + 20.986509, + 52.110085 + ], + [ + 20.986466, + 52.109925 + ], + [ + 20.98448, + 52.10562 + ], + [ + 20.98736, + 52.105139 + ], + [ + 20.986858, + 52.103902 + ], + [ + 20.991597, + 52.103114 + ], + [ + 20.991857, + 52.103165 + ], + [ + 20.991891, + 52.10336 + ], + [ + 20.991925, + 52.103457 + ], + [ + 20.993414, + 52.10348 + ], + [ + 20.99867, + 52.103464 + ], + [ + 20.999379, + 52.103472 + ], + [ + 21.000273, + 52.103456 + ], + [ + 21.001457, + 52.10348 + ], + [ + 21.004935, + 52.103468 + ], + [ + 21.007704, + 52.103545 + ], + [ + 21.00876, + 52.103559 + ], + [ + 21.009034, + 52.103552 + ], + [ + 21.00996, + 52.103566 + ], + [ + 21.011254, + 52.103564 + ], + [ + 21.011671, + 52.103573 + ], + [ + 21.011983, + 52.103575 + ], + [ + 21.012353, + 52.103581 + ], + [ + 21.013337, + 52.103581 + ], + [ + 21.01516, + 52.10362 + ], + [ + 21.018383, + 52.10366 + ], + [ + 21.018693, + 52.103684 + ], + [ + 21.018642, + 52.106049 + ], + [ + 21.021186, + 52.106043 + ], + [ + 21.024485, + 52.106082 + ], + [ + 21.035503, + 52.10619 + ], + [ + 21.035755, + 52.106401 + ], + [ + 21.035183, + 52.106733 + ], + [ + 21.037178, + 52.10809 + ], + [ + 21.037507, + 52.108078 + ], + [ + 21.038113, + 52.108355 + ], + [ + 21.037934, + 52.108612 + ], + [ + 21.041907, + 52.111303 + ], + [ + 21.0421, + 52.111924 + ], + [ + 21.043618, + 52.112904 + ], + [ + 21.043715, + 52.112963 + ], + [ + 21.04462, + 52.112676 + ], + [ + 21.046102, + 52.111985 + ], + [ + 21.046689, + 52.111607 + ], + [ + 21.047391, + 52.111189 + ], + [ + 21.048336, + 52.110704 + ], + [ + 21.048657, + 52.110261 + ], + [ + 21.050453, + 52.109153 + ], + [ + 21.050756, + 52.109012 + ], + [ + 21.051582, + 52.108705 + ], + [ + 21.051959, + 52.108567 + ], + [ + 21.052508, + 52.108407 + ], + [ + 21.054291, + 52.108046 + ], + [ + 21.05502, + 52.107908 + ], + [ + 21.0562, + 52.107644 + ], + [ + 21.056526, + 52.107726 + ], + [ + 21.058038, + 52.107205 + ], + [ + 21.058401, + 52.10705 + ], + [ + 21.060349, + 52.106288 + ], + [ + 21.062325, + 52.105423 + ], + [ + 21.063008, + 52.105026 + ], + [ + 21.064016, + 52.104516 + ], + [ + 21.064485, + 52.104315 + ], + [ + 21.065715, + 52.103905 + ], + [ + 21.06708, + 52.103306 + ], + [ + 21.068254, + 52.102714 + ], + [ + 21.069173, + 52.102272 + ], + [ + 21.069458, + 52.102169 + ], + [ + 21.070003, + 52.102009 + ], + [ + 21.070981, + 52.101635 + ], + [ + 21.072199, + 52.101114 + ], + [ + 21.07421, + 52.10036 + ], + [ + 21.07592, + 52.099523 + ], + [ + 21.076794, + 52.099219 + ], + [ + 21.077638, + 52.098645 + ], + [ + 21.078307, + 52.098291 + ], + [ + 21.078753, + 52.098118 + ], + [ + 21.07963, + 52.097923 + ], + [ + 21.079851, + 52.097893 + ], + [ + 21.079978, + 52.097909 + ], + [ + 21.079977, + 52.097851 + ], + [ + 21.082377, + 52.098526 + ], + [ + 21.082646, + 52.09868 + ], + [ + 21.082986, + 52.098947 + ], + [ + 21.08327, + 52.099227 + ], + [ + 21.083613, + 52.099594 + ], + [ + 21.085045, + 52.101522 + ], + [ + 21.085417, + 52.101862 + ], + [ + 21.085596, + 52.102003 + ], + [ + 21.08663, + 52.102821 + ], + [ + 21.087018, + 52.103068 + ], + [ + 21.087485, + 52.1033 + ], + [ + 21.089332, + 52.103946 + ], + [ + 21.091315, + 52.104483 + ], + [ + 21.091834, + 52.104616 + ], + [ + 21.091893, + 52.104644 + ], + [ + 21.092365, + 52.104642 + ], + [ + 21.094177, + 52.104481 + ], + [ + 21.094831, + 52.104482 + ], + [ + 21.09544, + 52.104507 + ], + [ + 21.095962, + 52.10455 + ], + [ + 21.096737, + 52.104678 + ], + [ + 21.097796, + 52.104787 + ], + [ + 21.099924, + 52.105164 + ], + [ + 21.100121, + 52.105179 + ], + [ + 21.100674, + 52.105366 + ], + [ + 21.101332, + 52.105634 + ], + [ + 21.101556, + 52.105685 + ], + [ + 21.101794, + 52.105709 + ], + [ + 21.102419, + 52.105678 + ], + [ + 21.103537, + 52.105756 + ], + [ + 21.103492, + 52.105866 + ], + [ + 21.10295, + 52.106407 + ], + [ + 21.102521, + 52.106925 + ], + [ + 21.102172, + 52.107273 + ], + [ + 21.102026, + 52.107396 + ], + [ + 21.101874, + 52.107507 + ], + [ + 21.101583, + 52.107685 + ], + [ + 21.101829, + 52.107816 + ], + [ + 21.101583, + 52.107934 + ], + [ + 21.101396, + 52.108006 + ], + [ + 21.101187, + 52.108076 + ], + [ + 21.100908, + 52.108138 + ], + [ + 21.100252, + 52.108091 + ], + [ + 21.099747, + 52.108222 + ], + [ + 21.09841, + 52.108644 + ], + [ + 21.098204, + 52.108807 + ], + [ + 21.097867, + 52.109748 + ], + [ + 21.097324, + 52.110368 + ], + [ + 21.096429, + 52.110609 + ], + [ + 21.096694, + 52.111044 + ], + [ + 21.096424, + 52.111353 + ], + [ + 21.096019, + 52.111406 + ], + [ + 21.095874, + 52.111723 + ], + [ + 21.09619, + 52.111976 + ], + [ + 21.094828, + 52.113891 + ], + [ + 21.089739, + 52.114149 + ], + [ + 21.088166, + 52.114388 + ], + [ + 21.088089, + 52.114849 + ], + [ + 21.09357, + 52.114646 + ], + [ + 21.09402, + 52.114717 + ], + [ + 21.092349, + 52.116597 + ], + [ + 21.091901, + 52.116597 + ], + [ + 21.091552, + 52.117638 + ], + [ + 21.090746, + 52.117627 + ], + [ + 21.090403, + 52.117384 + ], + [ + 21.089475, + 52.117417 + ], + [ + 21.088612, + 52.118188 + ], + [ + 21.091507, + 52.118489 + ], + [ + 21.091666, + 52.11965 + ], + [ + 21.092499, + 52.120414 + ], + [ + 21.092284, + 52.120458 + ], + [ + 21.091387, + 52.120425 + ], + [ + 21.091171, + 52.120565 + ], + [ + 21.090506, + 52.121663 + ], + [ + 21.089135, + 52.123158 + ], + [ + 21.088969, + 52.123413 + ], + [ + 21.088524, + 52.123971 + ], + [ + 21.088525, + 52.123969 + ], + [ + 21.088042, + 52.124409 + ], + [ + 21.087607, + 52.124867 + ], + [ + 21.086551, + 52.126119 + ], + [ + 21.08583, + 52.1269 + ], + [ + 21.083698, + 52.12856 + ], + [ + 21.08334, + 52.129015 + ], + [ + 21.082776, + 52.129748 + ], + [ + 21.082896, + 52.13002 + ], + [ + 21.082602, + 52.130484 + ], + [ + 21.082256, + 52.131036 + ], + [ + 21.081454, + 52.132502 + ], + [ + 21.081359, + 52.132813 + ], + [ + 21.081249, + 52.133019 + ], + [ + 21.081086, + 52.133342 + ], + [ + 21.080334, + 52.134494 + ], + [ + 21.079345, + 52.13594 + ], + [ + 21.077054, + 52.135208 + ], + [ + 21.075438, + 52.135437 + ], + [ + 21.073839, + 52.137327 + ], + [ + 21.072343, + 52.137189 + ], + [ + 21.072094, + 52.137488 + ], + [ + 21.071998, + 52.13919 + ], + [ + 21.071508, + 52.139953 + ], + [ + 21.067682, + 52.144416 + ], + [ + 21.069644, + 52.145175 + ], + [ + 21.06733, + 52.147473 + ], + [ + 21.06596, + 52.148587 + ], + [ + 21.063149, + 52.151049 + ], + [ + 21.062411, + 52.151624 + ], + [ + 21.062017, + 52.151879 + ], + [ + 21.061252, + 52.152402 + ], + [ + 21.061245, + 52.152673 + ], + [ + 21.061317, + 52.152868 + ], + [ + 21.061712, + 52.153209 + ], + [ + 21.061618, + 52.153664 + ], + [ + 21.061359, + 52.154104 + ], + [ + 21.060666, + 52.155458 + ], + [ + 21.057346, + 52.158893 + ], + [ + 21.055081, + 52.161234 + ], + [ + 21.052327, + 52.163877 + ], + [ + 21.051333, + 52.164235 + ], + [ + 21.046738, + 52.168129 + ], + [ + 21.04754, + 52.1683 + ], + [ + 21.049678, + 52.168663 + ], + [ + 21.051527, + 52.169029 + ], + [ + 21.051993, + 52.16909 + ], + [ + 21.053802, + 52.169404 + ], + [ + 21.051087, + 52.171355 + ], + [ + 21.04772, + 52.173153 + ], + [ + 21.045198, + 52.174504 + ], + [ + 21.043309, + 52.174937 + ], + [ + 21.043235, + 52.173865 + ], + [ + 21.043006, + 52.173354 + ], + [ + 21.042247, + 52.172372 + ], + [ + 21.040091, + 52.170727 + ], + [ + 21.039844, + 52.170346 + ], + [ + 21.038848, + 52.169487 + ], + [ + 21.037432, + 52.168752 + ], + [ + 21.035669, + 52.168135 + ], + [ + 21.030592, + 52.166784 + ], + [ + 21.029862, + 52.166716 + ], + [ + 21.028529, + 52.16743 + ], + [ + 21.027981, + 52.167576 + ], + [ + 21.027507, + 52.167649 + ], + [ + 21.025662, + 52.167661 + ], + [ + 21.025653, + 52.167526 + ], + [ + 21.025379, + 52.167363 + ], + [ + 21.024375, + 52.166802 + ], + [ + 21.024274, + 52.16646 + ], + [ + 21.021398, + 52.166471 + ], + [ + 21.020668, + 52.166584 + ], + [ + 21.019499, + 52.166803 + ], + [ + 21.019727, + 52.166123 + ], + [ + 21.01822, + 52.164215 + ], + [ + 21.01675, + 52.164698 + ], + [ + 21.016741, + 52.164799 + ], + [ + 21.015783, + 52.164794 + ], + [ + 21.015737, + 52.165495 + ], + [ + 21.012022, + 52.165473 + ], + [ + 21.011365, + 52.166939 + ], + [ + 21.009484, + 52.167422 + ], + [ + 21.008617, + 52.167882 + ], + [ + 21.007996, + 52.167057 + ], + [ + 21.003513, + 52.16809 + ], + [ + 20.997615, + 52.16946 + ], + [ + 20.996629, + 52.169522 + ], + [ + 20.988265, + 52.171464 + ], + [ + 20.987691, + 52.170582 + ], + [ + 20.986775, + 52.171133 + ], + [ + 20.986774, + 52.167047 + ], + [ + 20.986947, + 52.162632 + ], + [ + 20.987824, + 52.154582 + ], + [ + 20.988864, + 52.145499 + ], + [ + 20.989201, + 52.141895 + ], + [ + 20.988079, + 52.141877 + ], + [ + 20.987212, + 52.141945 + ], + [ + 20.984602, + 52.142115 + ], + [ + 20.983872, + 52.141987 + ], + [ + 20.983302, + 52.140869 + ], + [ + 20.98297, + 52.140431 + ], + [ + 20.984737, + 52.139196 + ], + [ + 20.97923, + 52.133852 + ], + [ + 20.979607, + 52.132763 + ], + [ + 20.979608, + 52.129474 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 29, + "name": "Ursynów" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.105418, + 52.194102 + ], + [ + 21.10593, + 52.192927 + ], + [ + 21.106711, + 52.191664 + ], + [ + 21.10839, + 52.189063 + ], + [ + 21.110223, + 52.186763 + ], + [ + 21.112241, + 52.184671 + ], + [ + 21.114717, + 52.182521 + ], + [ + 21.116889, + 52.180974 + ], + [ + 21.118969, + 52.17971 + ], + [ + 21.121172, + 52.178578 + ], + [ + 21.122917, + 52.177804 + ], + [ + 21.12662, + 52.176387 + ], + [ + 21.129313, + 52.175443 + ], + [ + 21.131547, + 52.174461 + ], + [ + 21.133994, + 52.17312 + ], + [ + 21.136593, + 52.171366 + ], + [ + 21.137907, + 52.170309 + ], + [ + 21.138945, + 52.169291 + ], + [ + 21.140074, + 52.167915 + ], + [ + 21.140989, + 52.166708 + ], + [ + 21.141721, + 52.165445 + ], + [ + 21.142359, + 52.16405 + ], + [ + 21.142875, + 52.162411 + ], + [ + 21.143024, + 52.161168 + ], + [ + 21.143266, + 52.160244 + ], + [ + 21.143569, + 52.159283 + ], + [ + 21.143995, + 52.158416 + ], + [ + 21.144666, + 52.157417 + ], + [ + 21.14552, + 52.156437 + ], + [ + 21.147077, + 52.155022 + ], + [ + 21.148636, + 52.153947 + ], + [ + 21.150225, + 52.153097 + ], + [ + 21.151968, + 52.152379 + ], + [ + 21.154537, + 52.151528 + ], + [ + 21.156831, + 52.15079 + ], + [ + 21.158482, + 52.150035 + ], + [ + 21.159674, + 52.149355 + ], + [ + 21.160926, + 52.148374 + ], + [ + 21.161902, + 52.147412 + ], + [ + 21.162512, + 52.146714 + ], + [ + 21.163121, + 52.145752 + ], + [ + 21.164958, + 52.144727 + ], + [ + 21.16817, + 52.145082 + ], + [ + 21.171423, + 52.145441 + ], + [ + 21.173696, + 52.146095 + ], + [ + 21.175924, + 52.146734 + ], + [ + 21.175853, + 52.148459 + ], + [ + 21.178336, + 52.148872 + ], + [ + 21.183356, + 52.149683 + ], + [ + 21.18586, + 52.15009 + ], + [ + 21.1884, + 52.15051 + ], + [ + 21.188577, + 52.149318 + ], + [ + 21.197843, + 52.150243 + ], + [ + 21.201337, + 52.150539 + ], + [ + 21.20239, + 52.150661 + ], + [ + 21.203337, + 52.150801 + ], + [ + 21.204237, + 52.150897 + ], + [ + 21.208519, + 52.151354 + ], + [ + 21.212268, + 52.151748 + ], + [ + 21.213657, + 52.151866 + ], + [ + 21.21486, + 52.151998 + ], + [ + 21.215126, + 52.151787 + ], + [ + 21.217032, + 52.152352 + ], + [ + 21.218206, + 52.152849 + ], + [ + 21.21901, + 52.153191 + ], + [ + 21.219609, + 52.153784 + ], + [ + 21.220876, + 52.154193 + ], + [ + 21.223998, + 52.155289 + ], + [ + 21.226263, + 52.156042 + ], + [ + 21.227247, + 52.156379 + ], + [ + 21.229067, + 52.15699 + ], + [ + 21.231836, + 52.157978 + ], + [ + 21.234723, + 52.15891 + ], + [ + 21.23647, + 52.158157 + ], + [ + 21.237846, + 52.157598 + ], + [ + 21.239189, + 52.157031 + ], + [ + 21.241377, + 52.156118 + ], + [ + 21.242453, + 52.155668 + ], + [ + 21.245072, + 52.154564 + ], + [ + 21.246978, + 52.153773 + ], + [ + 21.248752, + 52.153036 + ], + [ + 21.247139, + 52.151642 + ], + [ + 21.251401, + 52.149451 + ], + [ + 21.254925, + 52.147602 + ], + [ + 21.2554, + 52.147357 + ], + [ + 21.255741, + 52.147172 + ], + [ + 21.262131, + 52.152519 + ], + [ + 21.262338, + 52.152665 + ], + [ + 21.262579, + 52.153102 + ], + [ + 21.25403, + 52.157379 + ], + [ + 21.255562, + 52.158653 + ], + [ + 21.255856, + 52.159261 + ], + [ + 21.256604, + 52.160715 + ], + [ + 21.259316, + 52.161306 + ], + [ + 21.260368, + 52.162023 + ], + [ + 21.261096, + 52.164511 + ], + [ + 21.261459, + 52.165749 + ], + [ + 21.26235, + 52.166622 + ], + [ + 21.263114, + 52.167382 + ], + [ + 21.267686, + 52.167448 + ], + [ + 21.267617, + 52.16841 + ], + [ + 21.270803, + 52.168518 + ], + [ + 21.270317, + 52.169555 + ], + [ + 21.267526, + 52.170125 + ], + [ + 21.267335, + 52.173317 + ], + [ + 21.269569, + 52.17336 + ], + [ + 21.269959, + 52.172043 + ], + [ + 21.271151, + 52.172151 + ], + [ + 21.270231, + 52.173839 + ], + [ + 21.270037, + 52.173944 + ], + [ + 21.259765, + 52.182555 + ], + [ + 21.263379, + 52.183975 + ], + [ + 21.265383, + 52.186001 + ], + [ + 21.240229, + 52.19588 + ], + [ + 21.23876, + 52.19668 + ], + [ + 21.238807, + 52.197045 + ], + [ + 21.216283, + 52.197439 + ], + [ + 21.216207, + 52.200031 + ], + [ + 21.21571, + 52.200677 + ], + [ + 21.216373, + 52.201052 + ], + [ + 21.215781, + 52.201976 + ], + [ + 21.215507, + 52.202105 + ], + [ + 21.21614, + 52.202917 + ], + [ + 21.216466, + 52.203531 + ], + [ + 21.216634, + 52.204741 + ], + [ + 21.218411, + 52.205253 + ], + [ + 21.215844, + 52.208313 + ], + [ + 21.214621, + 52.209267 + ], + [ + 21.21529, + 52.21222 + ], + [ + 21.213246, + 52.214383 + ], + [ + 21.21215, + 52.214301 + ], + [ + 21.206519, + 52.22049 + ], + [ + 21.207282, + 52.220642 + ], + [ + 21.206983, + 52.222669 + ], + [ + 21.20708, + 52.223959 + ], + [ + 21.207326, + 52.227633 + ], + [ + 21.211905, + 52.230009 + ], + [ + 21.207123, + 52.233159 + ], + [ + 21.205162, + 52.233836 + ], + [ + 21.200917, + 52.234741 + ], + [ + 21.197992, + 52.234437 + ], + [ + 21.196988, + 52.234551 + ], + [ + 21.192442, + 52.236379 + ], + [ + 21.18839, + 52.238009 + ], + [ + 21.184035, + 52.239761 + ], + [ + 21.181074, + 52.241717 + ], + [ + 21.176528, + 52.245128 + ], + [ + 21.173388, + 52.243491 + ], + [ + 21.163613, + 52.24157 + ], + [ + 21.163202, + 52.24236 + ], + [ + 21.162226, + 52.242209 + ], + [ + 21.161711, + 52.243024 + ], + [ + 21.156435, + 52.241979 + ], + [ + 21.155556, + 52.241953 + ], + [ + 21.15327, + 52.241367 + ], + [ + 21.141764, + 52.239079 + ], + [ + 21.139446, + 52.243214 + ], + [ + 21.138191, + 52.243166 + ], + [ + 21.137425, + 52.242771 + ], + [ + 21.135659, + 52.241464 + ], + [ + 21.134108, + 52.240225 + ], + [ + 21.133459, + 52.239609 + ], + [ + 21.133133, + 52.239495 + ], + [ + 21.132421, + 52.239094 + ], + [ + 21.128367, + 52.235041 + ], + [ + 21.128443, + 52.234924 + ], + [ + 21.127953, + 52.234444 + ], + [ + 21.127898, + 52.234005 + ], + [ + 21.127761, + 52.233646 + ], + [ + 21.127387, + 52.233341 + ], + [ + 21.126252, + 52.232807 + ], + [ + 21.124675, + 52.232495 + ], + [ + 21.119478, + 52.232449 + ], + [ + 21.111992, + 52.227607 + ], + [ + 21.103592, + 52.222109 + ], + [ + 21.097814, + 52.218397 + ], + [ + 21.097473, + 52.218543 + ], + [ + 21.096963, + 52.218167 + ], + [ + 21.096721, + 52.217528 + ], + [ + 21.0929, + 52.215315 + ], + [ + 21.095505, + 52.21394 + ], + [ + 21.099168, + 52.211223 + ], + [ + 21.100762, + 52.20969 + ], + [ + 21.102515, + 52.207362 + ], + [ + 21.103477, + 52.205564 + ], + [ + 21.104089, + 52.204073 + ], + [ + 21.104726, + 52.201525 + ], + [ + 21.104661, + 52.197703 + ], + [ + 21.104827, + 52.196623 + ], + [ + 21.105418, + 52.194102 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 30, + "name": "Wawer" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 21.133223, + 52.123007 + ], + [ + 21.134728, + 52.123461 + ], + [ + 21.134971, + 52.125219 + ], + [ + 21.135408, + 52.12655 + ], + [ + 21.135801, + 52.127668 + ], + [ + 21.136539, + 52.128521 + ], + [ + 21.136789, + 52.128772 + ], + [ + 21.137081, + 52.12903 + ], + [ + 21.138182, + 52.13022 + ], + [ + 21.138904, + 52.131085 + ], + [ + 21.138762, + 52.131089 + ], + [ + 21.13866, + 52.131185 + ], + [ + 21.13872, + 52.131322 + ], + [ + 21.138893, + 52.131451 + ], + [ + 21.139164, + 52.131533 + ], + [ + 21.139417, + 52.131568 + ], + [ + 21.140134, + 52.131576 + ], + [ + 21.140236, + 52.131755 + ], + [ + 21.140512, + 52.131969 + ], + [ + 21.140549, + 52.132402 + ], + [ + 21.14029, + 52.132594 + ], + [ + 21.140124, + 52.132947 + ], + [ + 21.141704, + 52.13352 + ], + [ + 21.145168, + 52.135552 + ], + [ + 21.14783, + 52.138457 + ], + [ + 21.150202, + 52.139828 + ], + [ + 21.151865, + 52.139808 + ], + [ + 21.153652, + 52.140085 + ], + [ + 21.153942, + 52.140531 + ], + [ + 21.153119, + 52.142336 + ], + [ + 21.153229, + 52.142722 + ], + [ + 21.154685, + 52.143636 + ], + [ + 21.15936, + 52.144126 + ], + [ + 21.164958, + 52.144727 + ], + [ + 21.163121, + 52.145752 + ], + [ + 21.162512, + 52.146714 + ], + [ + 21.161902, + 52.147412 + ], + [ + 21.160926, + 52.148374 + ], + [ + 21.159674, + 52.149355 + ], + [ + 21.158482, + 52.150035 + ], + [ + 21.156831, + 52.15079 + ], + [ + 21.154537, + 52.151528 + ], + [ + 21.151968, + 52.152379 + ], + [ + 21.150225, + 52.153097 + ], + [ + 21.148636, + 52.153947 + ], + [ + 21.147077, + 52.155022 + ], + [ + 21.14552, + 52.156437 + ], + [ + 21.144666, + 52.157417 + ], + [ + 21.143995, + 52.158416 + ], + [ + 21.143569, + 52.159283 + ], + [ + 21.143266, + 52.160244 + ], + [ + 21.143024, + 52.161168 + ], + [ + 21.142875, + 52.162411 + ], + [ + 21.142359, + 52.16405 + ], + [ + 21.141721, + 52.165445 + ], + [ + 21.140989, + 52.166708 + ], + [ + 21.140074, + 52.167915 + ], + [ + 21.138945, + 52.169291 + ], + [ + 21.137907, + 52.170309 + ], + [ + 21.136593, + 52.171366 + ], + [ + 21.133994, + 52.17312 + ], + [ + 21.131547, + 52.174461 + ], + [ + 21.129313, + 52.175443 + ], + [ + 21.12662, + 52.176387 + ], + [ + 21.122917, + 52.177804 + ], + [ + 21.121172, + 52.178578 + ], + [ + 21.118969, + 52.17971 + ], + [ + 21.116889, + 52.180974 + ], + [ + 21.114717, + 52.182521 + ], + [ + 21.112241, + 52.184671 + ], + [ + 21.110223, + 52.186763 + ], + [ + 21.10839, + 52.189063 + ], + [ + 21.106711, + 52.191664 + ], + [ + 21.10593, + 52.192927 + ], + [ + 21.105418, + 52.194102 + ], + [ + 21.10474, + 52.193738 + ], + [ + 21.103866, + 52.193172 + ], + [ + 21.103379, + 52.192499 + ], + [ + 21.102593, + 52.19217 + ], + [ + 21.101583, + 52.191957 + ], + [ + 21.100373, + 52.191368 + ], + [ + 21.099862, + 52.19097 + ], + [ + 21.099599, + 52.190358 + ], + [ + 21.099648, + 52.190029 + ], + [ + 21.099946, + 52.189454 + ], + [ + 21.100069, + 52.188864 + ], + [ + 21.099567, + 52.187333 + ], + [ + 21.099005, + 52.186629 + ], + [ + 21.097932, + 52.186079 + ], + [ + 21.096475, + 52.185935 + ], + [ + 21.095502, + 52.185369 + ], + [ + 21.094574, + 52.182751 + ], + [ + 21.094511, + 52.182337 + ], + [ + 21.094659, + 52.181725 + ], + [ + 21.095006, + 52.180898 + ], + [ + 21.089836, + 52.180557 + ], + [ + 21.089439, + 52.181208 + ], + [ + 21.081473, + 52.18452 + ], + [ + 21.072731, + 52.179267 + ], + [ + 21.072563, + 52.178736 + ], + [ + 21.072084, + 52.17836 + ], + [ + 21.07083, + 52.177603 + ], + [ + 21.070057, + 52.177393 + ], + [ + 21.069758, + 52.176979 + ], + [ + 21.0694, + 52.176704 + ], + [ + 21.068823, + 52.176376 + ], + [ + 21.06782, + 52.176051 + ], + [ + 21.0657, + 52.175382 + ], + [ + 21.065495, + 52.175674 + ], + [ + 21.06534, + 52.175596 + ], + [ + 21.065255, + 52.175568 + ], + [ + 21.065584, + 52.175291 + ], + [ + 21.066048, + 52.174811 + ], + [ + 21.064999, + 52.174431 + ], + [ + 21.064079, + 52.174084 + ], + [ + 21.064216, + 52.173961 + ], + [ + 21.066215, + 52.172337 + ], + [ + 21.068461, + 52.170244 + ], + [ + 21.067861, + 52.169676 + ], + [ + 21.066078, + 52.169709 + ], + [ + 21.065585, + 52.169731 + ], + [ + 21.065017, + 52.169807 + ], + [ + 21.063499, + 52.170092 + ], + [ + 21.061782, + 52.170218 + ], + [ + 21.059959, + 52.170192 + ], + [ + 21.059686, + 52.170151 + ], + [ + 21.058818, + 52.169921 + ], + [ + 21.057929, + 52.169778 + ], + [ + 21.057146, + 52.169713 + ], + [ + 21.055559, + 52.169574 + ], + [ + 21.054544, + 52.169466 + ], + [ + 21.054241, + 52.169457 + ], + [ + 21.053802, + 52.169404 + ], + [ + 21.051993, + 52.16909 + ], + [ + 21.051527, + 52.169029 + ], + [ + 21.049678, + 52.168663 + ], + [ + 21.04754, + 52.1683 + ], + [ + 21.046738, + 52.168129 + ], + [ + 21.051333, + 52.164235 + ], + [ + 21.052327, + 52.163877 + ], + [ + 21.055081, + 52.161234 + ], + [ + 21.057346, + 52.158893 + ], + [ + 21.060666, + 52.155458 + ], + [ + 21.061359, + 52.154104 + ], + [ + 21.061618, + 52.153664 + ], + [ + 21.061712, + 52.153209 + ], + [ + 21.061317, + 52.152868 + ], + [ + 21.061245, + 52.152673 + ], + [ + 21.061252, + 52.152402 + ], + [ + 21.062017, + 52.151879 + ], + [ + 21.062411, + 52.151624 + ], + [ + 21.063149, + 52.151049 + ], + [ + 21.06596, + 52.148587 + ], + [ + 21.06733, + 52.147473 + ], + [ + 21.069644, + 52.145175 + ], + [ + 21.067682, + 52.144416 + ], + [ + 21.071508, + 52.139953 + ], + [ + 21.071998, + 52.13919 + ], + [ + 21.072094, + 52.137488 + ], + [ + 21.072343, + 52.137189 + ], + [ + 21.073839, + 52.137327 + ], + [ + 21.075438, + 52.135437 + ], + [ + 21.077054, + 52.135208 + ], + [ + 21.079345, + 52.13594 + ], + [ + 21.080334, + 52.134494 + ], + [ + 21.081086, + 52.133342 + ], + [ + 21.081249, + 52.133019 + ], + [ + 21.081359, + 52.132813 + ], + [ + 21.081454, + 52.132502 + ], + [ + 21.082256, + 52.131036 + ], + [ + 21.082602, + 52.130484 + ], + [ + 21.082896, + 52.13002 + ], + [ + 21.082776, + 52.129748 + ], + [ + 21.08334, + 52.129015 + ], + [ + 21.083698, + 52.12856 + ], + [ + 21.08583, + 52.1269 + ], + [ + 21.086551, + 52.126119 + ], + [ + 21.087607, + 52.124867 + ], + [ + 21.088042, + 52.124409 + ], + [ + 21.088525, + 52.123969 + ], + [ + 21.088524, + 52.123971 + ], + [ + 21.088969, + 52.123413 + ], + [ + 21.089135, + 52.123158 + ], + [ + 21.090506, + 52.121663 + ], + [ + 21.091171, + 52.120565 + ], + [ + 21.091387, + 52.120425 + ], + [ + 21.092284, + 52.120458 + ], + [ + 21.092499, + 52.120414 + ], + [ + 21.091666, + 52.11965 + ], + [ + 21.091507, + 52.118489 + ], + [ + 21.088612, + 52.118188 + ], + [ + 21.089475, + 52.117417 + ], + [ + 21.090403, + 52.117384 + ], + [ + 21.090746, + 52.117627 + ], + [ + 21.091552, + 52.117638 + ], + [ + 21.091901, + 52.116597 + ], + [ + 21.092349, + 52.116597 + ], + [ + 21.09402, + 52.114717 + ], + [ + 21.09357, + 52.114646 + ], + [ + 21.088089, + 52.114849 + ], + [ + 21.088166, + 52.114388 + ], + [ + 21.089739, + 52.114149 + ], + [ + 21.094828, + 52.113891 + ], + [ + 21.09619, + 52.111976 + ], + [ + 21.095874, + 52.111723 + ], + [ + 21.096019, + 52.111406 + ], + [ + 21.096424, + 52.111353 + ], + [ + 21.096694, + 52.111044 + ], + [ + 21.096429, + 52.110609 + ], + [ + 21.097324, + 52.110368 + ], + [ + 21.097867, + 52.109748 + ], + [ + 21.098204, + 52.108807 + ], + [ + 21.09841, + 52.108644 + ], + [ + 21.099747, + 52.108222 + ], + [ + 21.100252, + 52.108091 + ], + [ + 21.100908, + 52.108138 + ], + [ + 21.100658, + 52.108173 + ], + [ + 21.100097, + 52.108353 + ], + [ + 21.101928, + 52.10923 + ], + [ + 21.104767, + 52.110629 + ], + [ + 21.107316, + 52.111869 + ], + [ + 21.108591, + 52.112487 + ], + [ + 21.108897, + 52.112628 + ], + [ + 21.109624, + 52.11296 + ], + [ + 21.110097, + 52.113169 + ], + [ + 21.108874, + 52.114475 + ], + [ + 21.111239, + 52.115676 + ], + [ + 21.116041, + 52.118633 + ], + [ + 21.116539, + 52.118764 + ], + [ + 21.117869, + 52.11853 + ], + [ + 21.119157, + 52.118769 + ], + [ + 21.12095, + 52.119368 + ], + [ + 21.122853, + 52.120429 + ], + [ + 21.123807, + 52.121108 + ], + [ + 21.125131, + 52.122518 + ], + [ + 21.126001, + 52.124195 + ], + [ + 21.126369, + 52.124128 + ], + [ + 21.126827, + 52.12554 + ], + [ + 21.127131, + 52.126059 + ], + [ + 21.129526, + 52.127209 + ], + [ + 21.130017, + 52.126912 + ], + [ + 21.130687, + 52.125503 + ], + [ + 21.131464, + 52.124969 + ], + [ + 21.132412, + 52.12345 + ], + [ + 21.133223, + 52.123007 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 32, + "name": "Wilanów" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 20.98297, + 52.140431 + ], + [ + 20.983302, + 52.140869 + ], + [ + 20.983872, + 52.141987 + ], + [ + 20.984602, + 52.142115 + ], + [ + 20.987212, + 52.141945 + ], + [ + 20.988079, + 52.141877 + ], + [ + 20.989201, + 52.141895 + ], + [ + 20.988864, + 52.145499 + ], + [ + 20.987824, + 52.154582 + ], + [ + 20.986947, + 52.162632 + ], + [ + 20.986774, + 52.167047 + ], + [ + 20.986775, + 52.171133 + ], + [ + 20.986718, + 52.180401 + ], + [ + 20.986734, + 52.181452 + ], + [ + 20.986742, + 52.181572 + ], + [ + 20.98663, + 52.185511 + ], + [ + 20.986582, + 52.187198 + ], + [ + 20.986544, + 52.188519 + ], + [ + 20.986509, + 52.189189 + ], + [ + 20.986356, + 52.189859 + ], + [ + 20.986099, + 52.19034 + ], + [ + 20.985849, + 52.190691 + ], + [ + 20.98463, + 52.191756 + ], + [ + 20.984112, + 52.19217 + ], + [ + 20.983076, + 52.192602 + ], + [ + 20.981958, + 52.192872 + ], + [ + 20.964338, + 52.197118 + ], + [ + 20.947044, + 52.201047 + ], + [ + 20.946099, + 52.201372 + ], + [ + 20.94528, + 52.201763 + ], + [ + 20.944516, + 52.202352 + ], + [ + 20.943052, + 52.203654 + ], + [ + 20.942213, + 52.204319 + ], + [ + 20.938419, + 52.207988 + ], + [ + 20.937568, + 52.208749 + ], + [ + 20.937226, + 52.209059 + ], + [ + 20.93697, + 52.209219 + ], + [ + 20.936057, + 52.209779 + ], + [ + 20.935061, + 52.210319 + ], + [ + 20.934258, + 52.210684 + ], + [ + 20.933565, + 52.210908 + ], + [ + 20.933124, + 52.210996 + ], + [ + 20.932708, + 52.211064 + ], + [ + 20.932512, + 52.211385 + ], + [ + 20.932202, + 52.211335 + ], + [ + 20.930528, + 52.210791 + ], + [ + 20.927656, + 52.2098 + ], + [ + 20.926754, + 52.209535 + ], + [ + 20.925594, + 52.209477 + ], + [ + 20.925569, + 52.209912 + ], + [ + 20.924927, + 52.20995 + ], + [ + 20.923982, + 52.210144 + ], + [ + 20.923336, + 52.210472 + ], + [ + 20.922863, + 52.210869 + ], + [ + 20.922616, + 52.211253 + ], + [ + 20.922512, + 52.211688 + ], + [ + 20.922569, + 52.21211 + ], + [ + 20.922876, + 52.212634 + ], + [ + 20.923311, + 52.212949 + ], + [ + 20.923799, + 52.213208 + ], + [ + 20.924393, + 52.213385 + ], + [ + 20.924332, + 52.213504 + ], + [ + 20.923797, + 52.21375 + ], + [ + 20.921612, + 52.21354 + ], + [ + 20.91832, + 52.214555 + ], + [ + 20.916227, + 52.214391 + ], + [ + 20.914657, + 52.214584 + ], + [ + 20.91425, + 52.214527 + ], + [ + 20.914074, + 52.214079 + ], + [ + 20.89976, + 52.212348 + ], + [ + 20.887677, + 52.210793 + ], + [ + 20.885016, + 52.210332 + ], + [ + 20.88172, + 52.209677 + ], + [ + 20.881075, + 52.209412 + ], + [ + 20.880533, + 52.209002 + ], + [ + 20.881168, + 52.208946 + ], + [ + 20.88795, + 52.206256 + ], + [ + 20.893452, + 52.204057 + ], + [ + 20.896723, + 52.202722 + ], + [ + 20.897948, + 52.202301 + ], + [ + 20.898949, + 52.202027 + ], + [ + 20.900143, + 52.201707 + ], + [ + 20.899968, + 52.200047 + ], + [ + 20.900674, + 52.199519 + ], + [ + 20.901612, + 52.198994 + ], + [ + 20.902254, + 52.198683 + ], + [ + 20.90318, + 52.198326 + ], + [ + 20.904509, + 52.198043 + ], + [ + 20.9063, + 52.197604 + ], + [ + 20.908182, + 52.196789 + ], + [ + 20.910542, + 52.195975 + ], + [ + 20.910453, + 52.1957 + ], + [ + 20.912126, + 52.19428 + ], + [ + 20.913217, + 52.193721 + ], + [ + 20.915382, + 52.192733 + ], + [ + 20.91577, + 52.192751 + ], + [ + 20.9192, + 52.193762 + ], + [ + 20.917459, + 52.190635 + ], + [ + 20.916821, + 52.189488 + ], + [ + 20.916568, + 52.188818 + ], + [ + 20.914769, + 52.185843 + ], + [ + 20.913388, + 52.18346 + ], + [ + 20.911534, + 52.180093 + ], + [ + 20.911299, + 52.179726 + ], + [ + 20.911216, + 52.17956 + ], + [ + 20.911819, + 52.179444 + ], + [ + 20.914079, + 52.179018 + ], + [ + 20.91703, + 52.178452 + ], + [ + 20.916768, + 52.177806 + ], + [ + 20.917638, + 52.177655 + ], + [ + 20.918737, + 52.177447 + ], + [ + 20.918408, + 52.177057 + ], + [ + 20.918748, + 52.176977 + ], + [ + 20.918992, + 52.176871 + ], + [ + 20.919107, + 52.176751 + ], + [ + 20.919156, + 52.176476 + ], + [ + 20.919117, + 52.174282 + ], + [ + 20.919149, + 52.174011 + ], + [ + 20.919152, + 52.173322 + ], + [ + 20.919159, + 52.171576 + ], + [ + 20.919158, + 52.171083 + ], + [ + 20.919543, + 52.17105 + ], + [ + 20.920332, + 52.170996 + ], + [ + 20.920913, + 52.170966 + ], + [ + 20.921076, + 52.170864 + ], + [ + 20.921335, + 52.170704 + ], + [ + 20.921591, + 52.170545 + ], + [ + 20.921931, + 52.170149 + ], + [ + 20.922252, + 52.169779 + ], + [ + 20.922494, + 52.169726 + ], + [ + 20.922842, + 52.169562 + ], + [ + 20.922828, + 52.169462 + ], + [ + 20.923427, + 52.169262 + ], + [ + 20.923383, + 52.168988 + ], + [ + 20.92318, + 52.168868 + ], + [ + 20.923577, + 52.168766 + ], + [ + 20.92474, + 52.168445 + ], + [ + 20.924741, + 52.167886 + ], + [ + 20.924801, + 52.167693 + ], + [ + 20.924981, + 52.167474 + ], + [ + 20.925369, + 52.167226 + ], + [ + 20.923896, + 52.166157 + ], + [ + 20.925642, + 52.165616 + ], + [ + 20.927377, + 52.16503 + ], + [ + 20.928921, + 52.164544 + ], + [ + 20.930456, + 52.163954 + ], + [ + 20.929951, + 52.163323 + ], + [ + 20.932938, + 52.162018 + ], + [ + 20.934976, + 52.161202 + ], + [ + 20.937237, + 52.160296 + ], + [ + 20.93773, + 52.16013 + ], + [ + 20.938832, + 52.159802 + ], + [ + 20.942639, + 52.158643 + ], + [ + 20.94303, + 52.15854 + ], + [ + 20.944506, + 52.158248 + ], + [ + 20.94516, + 52.158061 + ], + [ + 20.946541, + 52.157374 + ], + [ + 20.947027, + 52.157173 + ], + [ + 20.947765, + 52.156999 + ], + [ + 20.953786, + 52.15615 + ], + [ + 20.953765, + 52.15299 + ], + [ + 20.953764, + 52.152951 + ], + [ + 20.954758, + 52.152537 + ], + [ + 20.957751, + 52.150454 + ], + [ + 20.958437, + 52.149904 + ], + [ + 20.962434, + 52.148089 + ], + [ + 20.964939, + 52.147192 + ], + [ + 20.96965, + 52.145377 + ], + [ + 20.970544, + 52.145194 + ], + [ + 20.97375, + 52.144537 + ], + [ + 20.975877, + 52.144128 + ], + [ + 20.97755, + 52.14349 + ], + [ + 20.98297, + 52.140431 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 33, + "name": "Włochy" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 20.981027, + 52.259436 + ], + [ + 20.981605, + 52.259601 + ], + [ + 20.982843, + 52.259592 + ], + [ + 20.983544, + 52.259653 + ], + [ + 20.984309, + 52.259683 + ], + [ + 20.984931, + 52.259749 + ], + [ + 20.985476, + 52.259773 + ], + [ + 20.986628, + 52.259763 + ], + [ + 20.986619, + 52.259638 + ], + [ + 20.98763, + 52.259604 + ], + [ + 20.988815, + 52.259587 + ], + [ + 20.988838, + 52.259543 + ], + [ + 20.988996, + 52.259551 + ], + [ + 20.989118, + 52.259552 + ], + [ + 20.989263, + 52.259548 + ], + [ + 20.989834, + 52.25955 + ], + [ + 20.990219, + 52.259542 + ], + [ + 20.990218, + 52.259491 + ], + [ + 20.994644, + 52.259399 + ], + [ + 20.99482, + 52.259445 + ], + [ + 20.995023, + 52.259391 + ], + [ + 20.995148, + 52.259388 + ], + [ + 20.997466, + 52.259335 + ], + [ + 20.997471, + 52.25943 + ], + [ + 20.99917, + 52.259395 + ], + [ + 20.999552, + 52.259092 + ], + [ + 20.999637, + 52.259073 + ], + [ + 21.000107, + 52.259055 + ], + [ + 21.000116, + 52.259101 + ], + [ + 21.00052, + 52.259089 + ], + [ + 21.002565, + 52.259223 + ], + [ + 21.003437, + 52.259386 + ], + [ + 21.003534, + 52.259357 + ], + [ + 21.003892, + 52.259445 + ], + [ + 21.004002, + 52.2595 + ], + [ + 21.004677, + 52.259659 + ], + [ + 21.004657, + 52.25969 + ], + [ + 21.005383, + 52.259887 + ], + [ + 21.005373, + 52.259905 + ], + [ + 21.005619, + 52.260216 + ], + [ + 21.005742, + 52.260256 + ], + [ + 21.005837, + 52.260264 + ], + [ + 21.00592, + 52.260163 + ], + [ + 21.006542, + 52.260355 + ], + [ + 21.006616, + 52.26026 + ], + [ + 21.009374, + 52.26121 + ], + [ + 21.008591, + 52.26223 + ], + [ + 21.007767, + 52.263433 + ], + [ + 21.006895, + 52.264913 + ], + [ + 21.006264, + 52.266158 + ], + [ + 21.005715, + 52.267427 + ], + [ + 21.005401, + 52.268307 + ], + [ + 21.005142, + 52.26912 + ], + [ + 21.004924, + 52.269937 + ], + [ + 21.004728, + 52.270809 + ], + [ + 21.004578, + 52.271678 + ], + [ + 21.004437, + 52.272887 + ], + [ + 21.004361, + 52.274536 + ], + [ + 21.00426, + 52.275481 + ], + [ + 21.004096, + 52.276407 + ], + [ + 21.003885, + 52.277251 + ], + [ + 21.003546, + 52.278318 + ], + [ + 21.00312, + 52.279365 + ], + [ + 21.00252, + 52.280549 + ], + [ + 21.001963, + 52.281488 + ], + [ + 21.00151, + 52.28215 + ], + [ + 21.001018, + 52.282821 + ], + [ + 21.000347, + 52.283645 + ], + [ + 20.999503, + 52.284572 + ], + [ + 20.998451, + 52.285594 + ], + [ + 20.997423, + 52.286489 + ], + [ + 20.996221, + 52.287339 + ], + [ + 20.993739, + 52.286241 + ], + [ + 20.990547, + 52.284752 + ], + [ + 20.989593, + 52.285465 + ], + [ + 20.988544, + 52.285813 + ], + [ + 20.987474, + 52.286097 + ], + [ + 20.98662, + 52.286394 + ], + [ + 20.985471, + 52.285691 + ], + [ + 20.985721, + 52.285157 + ], + [ + 20.985764, + 52.284913 + ], + [ + 20.985611, + 52.283798 + ], + [ + 20.985549, + 52.283685 + ], + [ + 20.985405, + 52.283522 + ], + [ + 20.98517, + 52.283377 + ], + [ + 20.976805, + 52.279334 + ], + [ + 20.976563, + 52.278926 + ], + [ + 20.972511, + 52.276824 + ], + [ + 20.970064, + 52.275447 + ], + [ + 20.966982, + 52.274138 + ], + [ + 20.966137, + 52.274448 + ], + [ + 20.965348, + 52.273919 + ], + [ + 20.964952, + 52.274113 + ], + [ + 20.964413, + 52.273742 + ], + [ + 20.964217, + 52.273202 + ], + [ + 20.963125, + 52.271463 + ], + [ + 20.960137, + 52.26915 + ], + [ + 20.958679, + 52.268346 + ], + [ + 20.95776, + 52.267351 + ], + [ + 20.95764, + 52.266963 + ], + [ + 20.955667, + 52.263483 + ], + [ + 20.954522, + 52.262423 + ], + [ + 20.953612, + 52.261851 + ], + [ + 20.953152, + 52.261518 + ], + [ + 20.951811, + 52.26136 + ], + [ + 20.950113, + 52.259608 + ], + [ + 20.947228, + 52.257094 + ], + [ + 20.947628, + 52.256501 + ], + [ + 20.948005, + 52.256772 + ], + [ + 20.95024, + 52.255874 + ], + [ + 20.950905, + 52.255662 + ], + [ + 20.95155, + 52.255532 + ], + [ + 20.95422, + 52.255386 + ], + [ + 20.954667, + 52.255261 + ], + [ + 20.956273, + 52.255468 + ], + [ + 20.95855, + 52.255786 + ], + [ + 20.960023, + 52.25583 + ], + [ + 20.96195, + 52.255176 + ], + [ + 20.962366, + 52.255085 + ], + [ + 20.96306, + 52.255027 + ], + [ + 20.963186, + 52.255021 + ], + [ + 20.963237, + 52.255006 + ], + [ + 20.963291, + 52.254988 + ], + [ + 20.964516, + 52.254378 + ], + [ + 20.96547, + 52.254303 + ], + [ + 20.965589, + 52.254311 + ], + [ + 20.966406, + 52.254574 + ], + [ + 20.969723, + 52.255731 + ], + [ + 20.970015, + 52.255585 + ], + [ + 20.970141, + 52.255601 + ], + [ + 20.970473, + 52.255716 + ], + [ + 20.970489, + 52.255794 + ], + [ + 20.971095, + 52.256042 + ], + [ + 20.972644, + 52.25658 + ], + [ + 20.973648, + 52.256959 + ], + [ + 20.973562, + 52.257068 + ], + [ + 20.979429, + 52.259138 + ], + [ + 20.979434, + 52.259135 + ], + [ + 20.979457, + 52.259143 + ], + [ + 20.979573, + 52.25902 + ], + [ + 20.981027, + 52.259436 + ] + ] + ] + ] + }, + "properties": { + "cartodb_id": 35, + "name": "Żoliborz" + } + } + ] +} diff --git a/python_pkg/anki_decks/warsaw_landmarks/__init__.py b/python_pkg/anki_decks/warsaw_landmarks/__init__.py new file mode 100644 index 0000000..67717a7 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_landmarks/__init__.py @@ -0,0 +1 @@ +"""Warsaw landmarks Anki flashcard generator.""" diff --git a/python_pkg/anki_decks/warsaw_landmarks/run.sh b/python_pkg/anki_decks/warsaw_landmarks/run.sh new file mode 100755 index 0000000..d2db000 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_landmarks/run.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Script to generate Warsaw Landmarks Anki deck + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="$SCRIPT_DIR/.venv" +PREVIEW_DIR="$SCRIPT_DIR/preview_images" + +echo "=== Warsaw Landmarks Anki Generator ===" +echo + +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment..." + python3 -m venv "$VENV_DIR" +fi + +echo "Activating virtual environment..." +source "$VENV_DIR/bin/activate" + +echo "Installing dependencies..." +pip install --quiet --upgrade pip +pip install --quiet matplotlib genanki geopandas requests shapely + +cd "$SCRIPT_DIR" + +# Create preview images directory +mkdir -p "$PREVIEW_DIR" + +python -m warsaw_landmarks_anki --output warsaw_landmarks.apkg --preview "$PREVIEW_DIR" --preview-count 5 + +echo +echo "Done! The Anki deck is at: $SCRIPT_DIR/warsaw_landmarks.apkg" +echo "Preview images are in: $PREVIEW_DIR" diff --git a/python_pkg/anki_decks/warsaw_landmarks/warsaw_landmarks_anki.py b/python_pkg/anki_decks/warsaw_landmarks/warsaw_landmarks_anki.py new file mode 100755 index 0000000..86978b1 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_landmarks/warsaw_landmarks_anki.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +"""Anki flashcard generator for Warsaw landmarks. + +Generates Anki-compatible flashcard decks with maps showing individual +Warsaw landmarks (monuments, museums, parks, historic sites). +""" + +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 + +sys.path.insert(0, str(Path(__file__).parent.parent)) +import genanki +from geo_data import get_warsaw_landmarks +import geopandas as gpd +import matplotlib.pyplot as plt + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +# Landmark marker color +LANDMARK_COLOR = "#9B59B6" # Purple + + +def load_warsaw_boundary() -> gpd.GeoDataFrame: + """Load Warsaw boundary from districts GeoJSON.""" + districts_path = ( + Path(__file__).parent.parent / "warsaw_districts" / "warszawa-dzielnice.geojson" + ) + if districts_path.exists(): + warsaw_gdf = gpd.read_file(districts_path) + warsaw_boundary = warsaw_gdf[warsaw_gdf["name"] == "Warszawa"] + if len(warsaw_boundary) == 0: + warsaw_boundary = gpd.GeoDataFrame( + geometry=[warsaw_gdf.union_all()], crs=warsaw_gdf.crs + ) + return warsaw_boundary + + msg = "Warsaw boundary data not found" + raise FileNotFoundError(msg) + + +def create_landmark_map( + landmark_gdf: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Warsaw with one landmark highlighted.""" + fig, ax = plt.subplots(figsize=(10, 10)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Warsaw as a plain gray shape + warsaw_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + warsaw_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=2) + + # Plot the landmark as a star marker + landmark_gdf.plot( + ax=ax, + color=LANDMARK_COLOR, + markersize=400, + marker="*", + alpha=0.9, + edgecolor="#1A1A1A", + linewidth=1.5, + ) + + # Set bounds to Warsaw + bounds = warsaw_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_landmark_image_bytes( + landmark_gdf: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, +) -> bytes: + """Generate a landmark map image as bytes.""" + fig = create_landmark_map(landmark_gdf, warsaw_boundary) + + 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( + landmarks: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, + deck_name: str = "Warsaw Landmarks", +) -> genanki.Package: + """Generate Anki package for Warsaw landmarks.""" + model_id_hash = hashlib.md5(f"warsaw_landmarks_{deck_name}".encode()) # noqa: S324 + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +""" + + my_model = genanki.Model( + model_id, + "Warsaw Landmark Model", + fields=[ + {"name": "LandmarkMap"}, + {"name": "LandmarkName"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{LandmarkMap}}
', + "afmt": '
{{LandmarkMap}}
' + '
' + '
{{LandmarkName}}
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + for _, row in landmarks.iterrows(): + landmark_name = row["name"] + landmark_gdf = gpd.GeoDataFrame([row], crs=landmarks.crs) + + image_data = generate_landmark_image_bytes(landmark_gdf, warsaw_boundary) + filename = f"landmark_{landmark_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', landmark_name], + tags=["geography", "warsaw", "landmarks"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Warsaw landmarks.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: warsaw_landmarks.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Warsaw Landmarks", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("warsaw_landmarks.apkg") + + try: + sys.stdout.write("Loading landmark data...\n") + landmarks = get_warsaw_landmarks() + warsaw_boundary = load_warsaw_boundary() + num_landmarks = len(landmarks) + + sys.stdout.write(f"Generating flashcards for {num_landmarks} landmarks...\n") + + package = generate_anki_package(landmarks, warsaw_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_landmarks = list(landmarks.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_landmarks)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_landmarks: + landmark_name = row["name"] + landmark_gdf = gpd.GeoDataFrame([row], crs=landmarks.crs) + image_data = generate_landmark_image_bytes( + landmark_gdf, warsaw_boundary + ) + safe_name = landmark_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Landmarks: {num_landmarks}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/warsaw_metro/__init__.py b/python_pkg/anki_decks/warsaw_metro/__init__.py new file mode 100644 index 0000000..f08d025 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_metro/__init__.py @@ -0,0 +1 @@ +"""Warsaw metro stations Anki flashcard generator.""" diff --git a/python_pkg/anki_decks/warsaw_metro/cache/warsaw_metro.geojson b/python_pkg/anki_decks/warsaw_metro/cache/warsaw_metro.geojson new file mode 100644 index 0000000..3952809 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_metro/cache/warsaw_metro.geojson @@ -0,0 +1,287 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "Ratusz-Arsena\u0142", + "line": "", + "osm_id": 35121250 + }, + "geometry": { "type": "Point", "coordinates": [21.0008823, 52.2452163] } + }, + { + "type": "Feature", + "properties": { "name": "Marymont", "line": "", "osm_id": 291414851 }, + "geometry": { "type": "Point", "coordinates": [20.9719399, 52.2715768] } + }, + { + "type": "Feature", + "properties": { + "name": "Stare Bielany", + "line": "", + "osm_id": 307389114 + }, + "geometry": { "type": "Point", "coordinates": [20.9493511, 52.2818277] } + }, + { + "type": "Feature", + "properties": { "name": "Wawrzyszew", "line": "", "osm_id": 307389115 }, + "geometry": { "type": "Point", "coordinates": [20.939515, 52.2863474] } + }, + { + "type": "Feature", + "properties": { "name": "M\u0142ociny", "line": "", "osm_id": 307389116 }, + "geometry": { "type": "Point", "coordinates": [20.9298678, 52.2907703] } + }, + { + "type": "Feature", + "properties": { + "name": "S\u0142odowiec", + "line": "", + "osm_id": 1958108834 + }, + "geometry": { "type": "Point", "coordinates": [20.9601259, 52.2768261] } + }, + { + "type": "Feature", + "properties": { + "name": "Dworzec Wile\u0144ski", + "line": "", + "osm_id": 3390187815 + }, + "geometry": { "type": "Point", "coordinates": [21.0357972, 52.2537771] } + }, + { + "type": "Feature", + "properties": { + "name": "Stadion Narodowy", + "line": "", + "osm_id": 3390197100 + }, + "geometry": { "type": "Point", "coordinates": [21.042847, 52.2468346] } + }, + { + "type": "Feature", + "properties": { + "name": "Centrum Nauki Kopernik", + "line": "", + "osm_id": 3390230685 + }, + "geometry": { "type": "Point", "coordinates": [21.0317878, 52.2399148] } + }, + { + "type": "Feature", + "properties": { + "name": "Nowy \u015awiat-Uniwersytet", + "line": "", + "osm_id": 3390237898 + }, + "geometry": { "type": "Point", "coordinates": [21.0168168, 52.2368196] } + }, + { + "type": "Feature", + "properties": { "name": "Rondo ONZ", "line": "", "osm_id": 3390266492 }, + "geometry": { "type": "Point", "coordinates": [20.9981024, 52.2330735] } + }, + { + "type": "Feature", + "properties": { + "name": "Rondo Daszy\u0144skiego", + "line": "", + "osm_id": 3390267294 + }, + "geometry": { "type": "Point", "coordinates": [20.9828946, 52.2300827] } + }, + { + "type": "Feature", + "properties": { "name": "Kabaty", "line": "", "osm_id": 3390283399 }, + "geometry": { "type": "Point", "coordinates": [21.0650711, 52.1320765] } + }, + { + "type": "Feature", + "properties": { + "name": "Stok\u0142osy", + "line": "", + "osm_id": 3390289961 + }, + "geometry": { "type": "Point", "coordinates": [21.0347233, 52.1560759] } + }, + { + "type": "Feature", + "properties": { "name": "Natolin", "line": "", "osm_id": 3390290324 }, + "geometry": { "type": "Point", "coordinates": [21.0564351, 52.1411007] } + }, + { + "type": "Feature", + "properties": { "name": "Imielin", "line": "", "osm_id": 3390290598 }, + "geometry": { "type": "Point", "coordinates": [21.0461062, 52.1493] } + }, + { + "type": "Feature", + "properties": { + "name": "Ursyn\u00f3w", + "line": "", + "osm_id": 3390304927 + }, + "geometry": { "type": "Point", "coordinates": [21.0276283, 52.1620456] } + }, + { + "type": "Feature", + "properties": { + "name": "S\u0142u\u017cew", + "line": "", + "osm_id": 3390306374 + }, + "geometry": { "type": "Point", "coordinates": [21.0262866, 52.1727624] } + }, + { + "type": "Feature", + "properties": { + "name": "Rac\u0142awicka", + "line": "", + "osm_id": 3390327068 + }, + "geometry": { "type": "Point", "coordinates": [21.0122349, 52.1988637] } + }, + { + "type": "Feature", + "properties": { "name": "Wierzbno", "line": "", "osm_id": 3390333313 }, + "geometry": { "type": "Point", "coordinates": [21.0167966, 52.1898719] } + }, + { + "type": "Feature", + "properties": { "name": "Wilanowska", "line": "", "osm_id": 3390336021 }, + "geometry": { "type": "Point", "coordinates": [21.0231452, 52.1818168] } + }, + { + "type": "Feature", + "properties": { + "name": "Pole Mokotowskie", + "line": "", + "osm_id": 3390342469 + }, + "geometry": { "type": "Point", "coordinates": [21.0079298, 52.2087775] } + }, + { + "type": "Feature", + "properties": { + "name": "Politechnika", + "line": "", + "osm_id": 3390361095 + }, + "geometry": { "type": "Point", "coordinates": [21.0153031, 52.2186581] } + }, + { + "type": "Feature", + "properties": { "name": "Centrum", "line": "", "osm_id": 3390365405 }, + "geometry": { "type": "Point", "coordinates": [21.010186, 52.2310069] } + }, + { + "type": "Feature", + "properties": { + "name": "Dworzec Gda\u0144ski", + "line": "", + "osm_id": 3416840991 + }, + "geometry": { "type": "Point", "coordinates": [20.9941857, 52.2580586] } + }, + { + "type": "Feature", + "properties": { + "name": "Plac Wilsona", + "line": "", + "osm_id": 3615569910 + }, + "geometry": { "type": "Point", "coordinates": [20.9844973, 52.2692619] } + }, + { + "type": "Feature", + "properties": { "name": "P\u0142ocka", "line": "", "osm_id": 4930657482 }, + "geometry": { "type": "Point", "coordinates": [20.9663847, 52.2324542] } + }, + { + "type": "Feature", + "properties": { "name": "Trocka", "line": "", "osm_id": 4930657487 }, + "geometry": { "type": "Point", "coordinates": [21.0550586, 52.2751021] } + }, + { + "type": "Feature", + "properties": { + "name": "\u015awi\u0119tokrzyska", + "line": "", + "osm_id": 5117464830 + }, + "geometry": { "type": "Point", "coordinates": [21.0078988, 52.2350954] } + }, + { + "type": "Feature", + "properties": { + "name": "Ksi\u0119cia Janusza", + "line": "", + "osm_id": 5907821373 + }, + "geometry": { "type": "Point", "coordinates": [20.9443771, 52.2391822] } + }, + { + "type": "Feature", + "properties": { + "name": "M\u0142yn\u00f3w", + "line": "", + "osm_id": 5907821374 + }, + "geometry": { "type": "Point", "coordinates": [20.9601048, 52.2376624] } + }, + { + "type": "Feature", + "properties": { "name": "Szwedzka", "line": "", "osm_id": 6053348692 }, + "geometry": { "type": "Point", "coordinates": [21.0455232, 52.2634709] } + }, + { + "type": "Feature", + "properties": { + "name": "Targ\u00f3wek Mieszkaniowy", + "line": "", + "osm_id": 6564265270 + }, + "geometry": { "type": "Point", "coordinates": [21.0513658, 52.2692518] } + }, + { + "type": "Feature", + "properties": { "name": "Bemowo", "line": "", "osm_id": 7362922676 }, + "geometry": { "type": "Point", "coordinates": [20.9154991, 52.2392071] } + }, + { + "type": "Feature", + "properties": { + "name": "Ulrych\u00f3w", + "line": "", + "osm_id": 7362922677 + }, + "geometry": { "type": "Point", "coordinates": [20.9298652, 52.2403314] } + }, + { + "type": "Feature", + "properties": { + "name": "Br\u00f3dno", + "line": "", + "osm_id": 10058224345 + }, + "geometry": { "type": "Point", "coordinates": [21.0289387, 52.293585] } + }, + { + "type": "Feature", + "properties": { + "name": "Kondratowicza", + "line": "", + "osm_id": 10058224348 + }, + "geometry": { "type": "Point", "coordinates": [21.0486889, 52.2920848] } + }, + { + "type": "Feature", + "properties": { "name": "Zacisze", "line": "", "osm_id": 10058224349 }, + "geometry": { "type": "Point", "coordinates": [21.062148, 52.2837496] } + } + ] +} diff --git a/python_pkg/anki_decks/warsaw_metro/run.sh b/python_pkg/anki_decks/warsaw_metro/run.sh new file mode 100755 index 0000000..8b05b6a --- /dev/null +++ b/python_pkg/anki_decks/warsaw_metro/run.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Script to generate Warsaw Metro Anki deck + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="$SCRIPT_DIR/.venv" +PREVIEW_DIR="$SCRIPT_DIR/preview_images" + +echo "=== Warsaw Metro Stations Anki Generator ===" +echo + +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment..." + python3 -m venv "$VENV_DIR" +fi + +echo "Activating virtual environment..." +source "$VENV_DIR/bin/activate" + +echo "Installing dependencies..." +pip install --quiet --upgrade pip +pip install --quiet matplotlib genanki geopandas requests shapely + +cd "$SCRIPT_DIR" + +# Create preview images directory +mkdir -p "$PREVIEW_DIR" + +python -m warsaw_metro_anki --output warsaw_metro.apkg --preview "$PREVIEW_DIR" --preview-count 5 + +echo +echo "Done! The Anki deck is at: $SCRIPT_DIR/warsaw_metro.apkg" +echo "Preview images are in: $PREVIEW_DIR" diff --git a/python_pkg/anki_decks/warsaw_metro/warsaw_metro_anki.py b/python_pkg/anki_decks/warsaw_metro/warsaw_metro_anki.py new file mode 100755 index 0000000..c8d7b50 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_metro/warsaw_metro_anki.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +"""Anki flashcard generator for Warsaw metro stations. + +Generates Anki-compatible flashcard decks with maps showing individual +Warsaw metro stations highlighted on a city map. +""" + +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 + +sys.path.insert(0, str(Path(__file__).parent.parent)) +import genanki +from geo_data import get_warsaw_metro_stations +import geopandas as gpd +import matplotlib.pyplot as plt + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +# Station marker color +STATION_COLOR = "#E74C3C" + + +def load_warsaw_boundary() -> gpd.GeoDataFrame: + """Load Warsaw boundary from districts GeoJSON. + + Returns: + GeoDataFrame with Warsaw boundary. + """ + districts_path = ( + Path(__file__).parent.parent / "warsaw_districts" / "warszawa-dzielnice.geojson" + ) + if districts_path.exists(): + warsaw_gdf = gpd.read_file(districts_path) + warsaw_boundary = warsaw_gdf[warsaw_gdf["name"] == "Warszawa"] + if len(warsaw_boundary) == 0: + warsaw_boundary = gpd.GeoDataFrame( + geometry=[warsaw_gdf.union_all()], crs=warsaw_gdf.crs + ) + return warsaw_boundary + + msg = "Warsaw boundary data not found" + raise FileNotFoundError(msg) + + +def create_station_map( + station_gdf: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Warsaw with one metro station highlighted. + + Args: + station_gdf: GeoDataFrame with the station point. + warsaw_boundary: GeoDataFrame with Warsaw boundary. + + Returns: + A matplotlib Figure object. + """ + fig, ax = plt.subplots(figsize=(10, 10)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Warsaw as a plain gray shape + warsaw_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + warsaw_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=2) + + # Plot the station as a large dot + station_gdf.plot( + ax=ax, + color=STATION_COLOR, + markersize=300, + marker="o", + alpha=0.9, + edgecolor="#1A1A1A", + linewidth=2, + ) + + # Set bounds to Warsaw + bounds = warsaw_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_station_image_bytes( + station_gdf: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, +) -> bytes: + """Generate a station map image as bytes.""" + fig = create_station_map(station_gdf, warsaw_boundary) + + 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( + stations: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, + deck_name: str = "Warsaw Metro Stations", +) -> genanki.Package: + """Generate Anki package for Warsaw metro stations.""" + model_id_hash = hashlib.md5(f"warsaw_metro_{deck_name}".encode()) # noqa: S324 + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.line-info { + font-size: 24px; + margin-top: 10px; + color: #666; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +.card.night_mode .line-info { + color: #AAA; +} +""" + + my_model = genanki.Model( + model_id, + "Warsaw Metro Model", + fields=[ + {"name": "StationMap"}, + {"name": "StationName"}, + {"name": "Line"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{StationMap}}
', + "afmt": '
{{StationMap}}
' + '
' + '
{{StationName}}
' + '
{{Line}}
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + for _, row in stations.iterrows(): + station_name = row["name"] + line = row.get("line", "") + station_gdf = gpd.GeoDataFrame([row], crs=stations.crs) + + image_data = generate_station_image_bytes(station_gdf, warsaw_boundary) + filename = f"metro_{station_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', station_name, line], + tags=["geography", "warsaw", "metro"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Warsaw metro stations.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: warsaw_metro.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Warsaw Metro Stations", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("warsaw_metro.apkg") + + try: + sys.stdout.write("Loading metro station data...\n") + stations = get_warsaw_metro_stations() + warsaw_boundary = load_warsaw_boundary() + num_stations = len(stations) + + sys.stdout.write( + f"Generating flashcards for {num_stations} metro stations...\n" + ) + + package = generate_anki_package(stations, warsaw_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_stations = list(stations.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_stations)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_stations: + station_name = row["name"] + station_gdf = gpd.GeoDataFrame([row], crs=stations.crs) + image_data = generate_station_image_bytes(station_gdf, warsaw_boundary) + safe_name = station_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Stations: {num_stations}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/warsaw_osiedla/__init__.py b/python_pkg/anki_decks/warsaw_osiedla/__init__.py new file mode 100644 index 0000000..f355d33 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_osiedla/__init__.py @@ -0,0 +1 @@ +"""Warsaw osiedla (neighborhoods) Anki flashcard generator.""" diff --git a/python_pkg/anki_decks/warsaw_osiedla/run.sh b/python_pkg/anki_decks/warsaw_osiedla/run.sh new file mode 100755 index 0000000..17bc8ba --- /dev/null +++ b/python_pkg/anki_decks/warsaw_osiedla/run.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Script to generate Warsaw Osiedla Anki deck + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="$SCRIPT_DIR/.venv" +PREVIEW_DIR="$SCRIPT_DIR/preview_images" + +echo "=== Warsaw Osiedla Anki Generator ===" +echo + +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment..." + python3 -m venv "$VENV_DIR" +fi + +echo "Activating virtual environment..." +source "$VENV_DIR/bin/activate" + +echo "Installing dependencies..." +pip install --quiet --upgrade pip +pip install --quiet matplotlib genanki geopandas requests shapely + +cd "$SCRIPT_DIR" + +# Create preview images directory +mkdir -p "$PREVIEW_DIR" + +python -m warsaw_osiedla_anki --output warsaw_osiedla.apkg --preview "$PREVIEW_DIR" --preview-count 5 + +echo +echo "Done! The Anki deck is at: $SCRIPT_DIR/warsaw_osiedla.apkg" +echo "Preview images are in: $PREVIEW_DIR" diff --git a/python_pkg/anki_decks/warsaw_osiedla/warsaw_osiedla_anki.py b/python_pkg/anki_decks/warsaw_osiedla/warsaw_osiedla_anki.py new file mode 100755 index 0000000..5a3ee77 --- /dev/null +++ b/python_pkg/anki_decks/warsaw_osiedla/warsaw_osiedla_anki.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 +"""Anki flashcard generator for Warsaw osiedla (neighborhoods). + +Generates Anki-compatible flashcard decks with maps showing individual +Warsaw neighborhoods highlighted on a city map. +""" + +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 + +sys.path.insert(0, str(Path(__file__).parent.parent)) +import genanki +from geo_data import get_warsaw_osiedla +import geopandas as gpd +import matplotlib.pyplot as plt + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +# 50 unique colors for neighborhoods +OSIEDLE_COLORS = [ + "#E74C3C", + "#3498DB", + "#2ECC71", + "#9B59B6", + "#F39C12", + "#1ABC9C", + "#E91E63", + "#00BCD4", + "#8BC34A", + "#FF5722", + "#673AB7", + "#FFEB3B", + "#795548", + "#607D8B", + "#CDDC39", + "#FF9800", + "#4CAF50", + "#03A9F4", + "#F44336", + "#009688", + "#3F51B5", + "#FFC107", + "#9E9E9E", + "#00E676", + "#FF4081", + "#448AFF", + "#69F0AE", + "#FFD740", + "#40C4FF", + "#B388FF", + "#EA80FC", + "#82B1FF", + "#A7FFEB", + "#FFFF8D", + "#FF80AB", + "#536DFE", + "#64FFDA", + "#FFE57F", + "#80D8FF", + "#B9F6CA", + "#CF6679", + "#BB86FC", + "#03DAC6", + "#018786", + "#6200EE", + "#3700B3", + "#B00020", + "#FF0266", + "#C51162", + "#AA00FF", +] + + +def load_warsaw_boundary() -> gpd.GeoDataFrame: + """Load Warsaw boundary from districts GeoJSON.""" + districts_path = ( + Path(__file__).parent.parent / "warsaw_districts" / "warszawa-dzielnice.geojson" + ) + if districts_path.exists(): + warsaw_gdf = gpd.read_file(districts_path) + warsaw_boundary = warsaw_gdf[warsaw_gdf["name"] == "Warszawa"] + if len(warsaw_boundary) == 0: + warsaw_boundary = gpd.GeoDataFrame( + geometry=[warsaw_gdf.union_all()], crs=warsaw_gdf.crs + ) + return warsaw_boundary + + msg = "Warsaw boundary data not found" + raise FileNotFoundError(msg) + + +def create_osiedle_map( + osiedle_name: str, + osiedle_gdf: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, + all_osiedla: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Warsaw with one osiedle highlighted.""" + fig, ax = plt.subplots(figsize=(10, 10)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Warsaw as a plain gray shape + warsaw_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + warsaw_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=2) + + # Assign color based on sorted names + sorted_names = sorted(all_osiedla["name"].tolist()) + color_idx = sorted_names.index(osiedle_name) % len(OSIEDLE_COLORS) + fill_color = OSIEDLE_COLORS[color_idx] + + # Plot the highlighted osiedle + osiedle_gdf.plot(ax=ax, color=fill_color, alpha=0.9) + osiedle_gdf.boundary.plot(ax=ax, color="#1A1A1A", linewidth=4) + + # Set bounds to Warsaw + bounds = warsaw_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_osiedle_image_bytes( + osiedle_name: str, + osiedle_gdf: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, + all_osiedla: gpd.GeoDataFrame, +) -> bytes: + """Generate an osiedle map image as bytes.""" + fig = create_osiedle_map(osiedle_name, osiedle_gdf, warsaw_boundary, all_osiedla) + + 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( + osiedla: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, + deck_name: str = "Warsaw Osiedla", +) -> genanki.Package: + """Generate Anki package for Warsaw osiedla.""" + model_id_hash = hashlib.md5(f"warsaw_osiedla_{deck_name}".encode()) # noqa: S324 + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +""" + + my_model = genanki.Model( + model_id, + "Warsaw Osiedle Model", + fields=[ + {"name": "OsiedleMap"}, + {"name": "OsiedleName"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{OsiedleMap}}
', + "afmt": '
{{OsiedleMap}}
' + '
' + '
{{OsiedleName}}
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + for _, row in osiedla.iterrows(): + osiedle_name = row["name"] + osiedle_gdf = gpd.GeoDataFrame([row], crs=osiedla.crs) + + image_data = generate_osiedle_image_bytes( + osiedle_name, osiedle_gdf, warsaw_boundary, osiedla + ) + filename = f"osiedle_{osiedle_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', osiedle_name], + tags=["geography", "warsaw", "osiedla"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Warsaw osiedla.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: warsaw_osiedla.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Warsaw Osiedla", + help="Name for the Anki deck", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("warsaw_osiedla.apkg") + + try: + sys.stdout.write("Loading osiedla data...\n") + osiedla = get_warsaw_osiedla() + warsaw_boundary = load_warsaw_boundary() + num_osiedla = len(osiedla) + + sys.stdout.write(f"Generating flashcards for {num_osiedla} osiedla...\n") + + package = generate_anki_package(osiedla, warsaw_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_osiedla = list(osiedla.iterrows())[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_osiedla)} preview images " + f"to {preview_dir}...\n" + ) + for _, row in preview_osiedla: + osiedle_name = row["name"] + osiedle_gdf = gpd.GeoDataFrame([row], crs=osiedla.crs) + image_data = generate_osiedle_image_bytes( + osiedle_name, osiedle_gdf, warsaw_boundary, osiedla + ) + safe_name = osiedle_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name}\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Osiedla: {num_osiedla}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/anki_decks/warsaw_streets/README.md b/python_pkg/anki_decks/warsaw_streets/README.md new file mode 100644 index 0000000..f6ac8ec --- /dev/null +++ b/python_pkg/anki_decks/warsaw_streets/README.md @@ -0,0 +1,37 @@ +# Warsaw Streets Anki Generator + +Generate Anki flashcards for learning major Warsaw streets. + +## Features + +- Generates flashcards for major Warsaw streets (primary, secondary, tertiary roads) +- Uses real street data from OpenStreetMap +- Front of card: Map showing Warsaw with the street highlighted +- Back of card: Street name in Polish +- Self-contained .apkg file with embedded images + +## Data Source + +Street data is fetched from OpenStreetMap via the Overpass API. + +## Installation + +```bash +pip install matplotlib genanki geopandas requests shapely +``` + +## Usage + +```bash +# Generate flashcards (fetches data from OSM) +./run.sh + +# Or run directly +python -m warsaw_streets_anki +``` + +## Notes + +- Only includes named streets tagged as primary, secondary, or tertiary highways +- Streets are filtered to remove duplicates and very short segments +- The first run will download data from Overpass API (may take a minute) diff --git a/python_pkg/anki_decks/warsaw_streets/__init__.py b/python_pkg/anki_decks/warsaw_streets/__init__.py new file mode 100644 index 0000000..d71b26e --- /dev/null +++ b/python_pkg/anki_decks/warsaw_streets/__init__.py @@ -0,0 +1 @@ +"""Warsaw streets Anki flashcard generator.""" diff --git a/python_pkg/anki_decks/warsaw_streets/run.sh b/python_pkg/anki_decks/warsaw_streets/run.sh new file mode 100755 index 0000000..439958e --- /dev/null +++ b/python_pkg/anki_decks/warsaw_streets/run.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Script to generate Warsaw Streets Anki deck + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="$SCRIPT_DIR/.venv" +PREVIEW_DIR="$SCRIPT_DIR/preview_images" + +echo "=== Warsaw Streets Anki Generator ===" +echo + +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment..." + python3 -m venv "$VENV_DIR" +fi + +echo "Activating virtual environment..." +source "$VENV_DIR/bin/activate" + +echo "Installing dependencies..." +pip install --quiet --upgrade pip +pip install --quiet matplotlib genanki geopandas requests shapely + +cd "$SCRIPT_DIR" + +# Create preview images directory +mkdir -p "$PREVIEW_DIR" + +python -m warsaw_streets_anki --output warsaw_streets.apkg --preview "$PREVIEW_DIR" --preview-count 5 + +echo +echo "Done! The Anki deck is at: $SCRIPT_DIR/warsaw_streets.apkg" +echo "Preview images are in: $PREVIEW_DIR" diff --git a/python_pkg/anki_decks/warsaw_streets/warsaw_streets_anki.py b/python_pkg/anki_decks/warsaw_streets/warsaw_streets_anki.py new file mode 100755 index 0000000..2eb304c --- /dev/null +++ b/python_pkg/anki_decks/warsaw_streets/warsaw_streets_anki.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 +"""Anki flashcard generator for Warsaw streets. + +Generates Anki-compatible flashcard decks with maps showing individual +Warsaw streets highlighted on a city map. + +Usage: + python -m python_pkg.anki_decks.warsaw_streets.warsaw_streets_anki + +Output: + Creates a self-contained .apkg file that can be directly imported into Anki. +""" + +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, Any + +sys.path.insert(0, str(Path(__file__).parent.parent)) +import genanki +from geo_data import get_warsaw_streets +import geopandas as gpd +import matplotlib.pyplot as plt +from shapely.geometry import MultiLineString + +if TYPE_CHECKING: + from collections.abc import Sequence + + from matplotlib.figure import Figure + +# Minimum street length in meters to include +MIN_STREET_LENGTH = 500 + + +def get_unique_streets( + gdf: gpd.GeoDataFrame, +) -> list[tuple[str, gpd.GeoDataFrame, float]]: + """Group street segments by name and merge geometries. + + Args: + gdf: GeoDataFrame with street segments. + + Returns: + List of (name, GeoDataFrame, length_m) tuples, sorted by length (longest first). + """ + # Group by street name + streets: dict[str, list[Any]] = {} + for _, row in gdf.iterrows(): + name = row["name"] + if name and name != "Unknown": + if name not in streets: + streets[name] = [] + streets[name].append(row.geometry) + + # Merge geometries and calculate length + result = [] + for name, geometries in streets.items(): + merged = geometries[0] if len(geometries) == 1 else MultiLineString(geometries) + + # Create a GeoDataFrame for this street + street_gdf = gpd.GeoDataFrame( + [{"name": name, "geometry": merged}], crs="EPSG:4326" + ) + + # Calculate length in meters (approximate) + street_gdf_proj = street_gdf.to_crs("EPSG:2180") # Polish coordinate system + length = street_gdf_proj.geometry.length.iloc[0] + + if length >= MIN_STREET_LENGTH: + result.append((name, street_gdf, length)) + + # Sort by length (longest first) + result.sort(key=lambda x: x[2], reverse=True) + return result + + +def load_street_data() -> ( + tuple[list[tuple[str, gpd.GeoDataFrame, float]], gpd.GeoDataFrame] +): + """Load Warsaw streets and boundary. + + Returns: + Tuple of (streets list sorted by length, warsaw boundary GeoDataFrame). + """ + streets_gdf = get_warsaw_streets(min_length=MIN_STREET_LENGTH) + streets = get_unique_streets(streets_gdf) + + # Load Warsaw districts for boundary (reuse from warsaw_districts) + districts_path = ( + Path(__file__).parent.parent / "warsaw_districts" / "warszawa-dzielnice.geojson" + ) + if districts_path.exists(): + warsaw_gdf = gpd.read_file(districts_path) + # Get just Warsaw boundary + warsaw_boundary = warsaw_gdf[warsaw_gdf["name"] == "Warszawa"] + if len(warsaw_boundary) == 0: + # Dissolve all districts + warsaw_boundary = gpd.GeoDataFrame( + geometry=[warsaw_gdf.union_all()], crs=warsaw_gdf.crs + ) + else: + msg = "Warsaw boundary data not found" + raise FileNotFoundError(msg) + + return streets, warsaw_boundary + + +# Color for highlighted street +STREET_COLOR = "#E74C3C" # Red + + +def create_street_map( + street_gdf: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, +) -> Figure: + """Create a map showing Warsaw with one street highlighted. + + Args: + street_name: Name of the street. + street_gdf: GeoDataFrame with the street geometry. + warsaw_boundary: GeoDataFrame with Warsaw boundary. + + Returns: + A matplotlib Figure object. + """ + fig, ax = plt.subplots(figsize=(10, 10)) + ax.set_aspect("equal") + ax.axis("off") + fig.patch.set_alpha(0) + ax.patch.set_alpha(0) + + # Plot Warsaw as a plain gray shape + warsaw_boundary.plot(ax=ax, color="#D5D8DC", alpha=0.6) + warsaw_boundary.boundary.plot(ax=ax, color="#2C3E50", linewidth=2) + + # Plot the highlighted street + street_gdf.plot(ax=ax, color=STREET_COLOR, linewidth=4, alpha=0.9) + + # Set bounds to Warsaw + bounds = warsaw_boundary.total_bounds + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig + + +def generate_street_image_bytes( + street_gdf: gpd.GeoDataFrame, + warsaw_boundary: gpd.GeoDataFrame, +) -> bytes: + """Generate a street map image as bytes. + + Args: + street_gdf: GeoDataFrame with the street geometry. + warsaw_boundary: GeoDataFrame with Warsaw boundary. + + Returns: + PNG image data as bytes. + """ + fig = create_street_map(street_gdf, warsaw_boundary) + + 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( + streets: list[tuple[str, gpd.GeoDataFrame, float]], + warsaw_boundary: gpd.GeoDataFrame, + deck_name: str = "Warsaw Streets", +) -> genanki.Package: + """Generate Anki package for Warsaw streets. + + Args: + streets: List of (name, GeoDataFrame, length) tuples, sorted by length. + warsaw_boundary: GeoDataFrame with Warsaw boundary. + deck_name: Name for the Anki deck. + + Returns: + genanki.Package object. + """ + model_id_hash = hashlib.md5(f"warsaw_streets_{deck_name}".encode()) # noqa: S324 + model_id = int(model_id_hash.hexdigest()[:8], 16) + + card_css = """ +.card { + font-family: Arial, sans-serif; + font-size: 24px; + text-align: center; + color: #333; + background-color: #fff; +} +.card.night_mode { + color: #eee; + background-color: #2f2f2f; +} +.map-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.map-container img { + max-width: 100%; + max-height: 80vh; + object-fit: contain; +} +.answer-text { + font-size: 32px; + font-weight: bold; + margin-top: 20px; + color: #2C3E50; +} +.card.night_mode .answer-text { + color: #ECF0F1; +} +""" + + my_model = genanki.Model( + model_id, + "Warsaw Street Model", + fields=[ + {"name": "StreetMap"}, + {"name": "StreetName"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": '
{{StreetMap}}
', + "afmt": '
{{StreetMap}}
' + '
' + '
{{StreetName}}
', + }, + ], + css=card_css, + ) + + deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311 + my_deck = genanki.Deck(deck_id, deck_name) + media_files = [] + + # Streets are already sorted by length (longest first) + for street_name, street_gdf, _length in streets: + image_data = generate_street_image_bytes(street_gdf, warsaw_boundary) + filename = f"street_{street_name.replace(' ', '_').replace('/', '_')}.png" + + note = genanki.Note( + model=my_model, + fields=[f'', street_name], + tags=["geography", "warsaw", "streets"], + ) + my_deck.add_note(note) + + temp_path = Path(f"/tmp/{filename}") # noqa: S108 + temp_path.write_bytes(image_data) + media_files.append(str(temp_path)) + + package = genanki.Package(my_deck) + package.media_files = media_files + return package + + +def main(argv: Sequence[str] | None = None) -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate Anki flashcards for Warsaw streets.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (default: warsaw_streets.apkg)", + ) + parser.add_argument( + "--deck-name", + "-d", + type=str, + default="Warsaw Streets", + help="Name for the Anki deck", + ) + parser.add_argument( + "--min-length", + "-m", + type=int, + default=MIN_STREET_LENGTH, + help=f"Minimum street length in meters (default: {MIN_STREET_LENGTH})", + ) + parser.add_argument( + "--preview", + "-p", + type=str, + default=None, + help="Export preview images to specified directory", + ) + parser.add_argument( + "--preview-count", + type=int, + default=5, + help="Number of preview images to export (default: 5)", + ) + + args = parser.parse_args(argv) + output_path = Path(args.output) if args.output else Path("warsaw_streets.apkg") + + try: + sys.stdout.write("Loading street data...\n") + streets, warsaw_boundary = load_street_data() + num_streets = len(streets) + + sys.stdout.write(f"Generating flashcards for {num_streets} Warsaw streets...\n") + + package = generate_anki_package(streets, warsaw_boundary, args.deck_name) + package.write_to_file(str(output_path)) + + # Export preview images if requested (top N longest streets) + if args.preview: + preview_dir = Path(args.preview) + preview_dir.mkdir(parents=True, exist_ok=True) + preview_streets = streets[: args.preview_count] + sys.stdout.write( + f"Exporting {len(preview_streets)} preview images " + f"(longest streets) to {preview_dir}...\n" + ) + for street_name, street_gdf, length_m in preview_streets: + image_data = generate_street_image_bytes(street_gdf, warsaw_boundary) + safe_name = street_name.replace(" ", "_").replace("/", "_") + preview_path = preview_dir / f"{safe_name}.png" + preview_path.write_bytes(image_data) + sys.stdout.write(f" Saved: {preview_path.name} ({length_m:.0f}m)\n") + + sys.stdout.write("\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write("FLASHCARD GENERATION COMPLETE\n") + sys.stdout.write("=" * 60 + "\n") + sys.stdout.write(f"Streets: {num_streets}\n") + sys.stdout.write(f"Output file: {output_path.absolute()}\n") + if args.preview: + sys.stdout.write(f"Preview images: {args.preview}\n") + except (OSError, ValueError, RuntimeError) as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main())