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)
+
+

Wał Miedzeszyński (27.8km)
+

Puławska (25.3km)
+

Aleje Jerozolimskie (22.2km)
+

Modlińska (19.2km)
+

Patriotów (15.9km)
+
+
+ 🚇 Warsaw Metro Stations
+
+

Ratusz-Arsenał
+

Marymont
+

Stare Bielany
+

Wawrzyszew
+

Młociny
+
+
+ 🌉 Warsaw Bridges
+
+

Most Siekierkowski
+

Most Świętokrzyski
+

Most Grota-Roweckiego
+

Most M. Skłodowskiej-Curie
+

Most Anny Jagiellonki
+
+
+ 🏛️ Warsaw Landmarks
+
+

Pomnik Kościuszkowców
+

Pomnik Państwa Podziemnego i AK
+

Muzeum Etnograficzne
+

Żydowski Instytut Historyczny
+

Muzeum M. Skłodowskiej-Curie
+
+
+ 🏘️ Warsaw Osiedla
+
+

Grochów-Centrum
+

Grochów-Kinowa
+

Grochów-Południowy
+

Grochów-Północny
+

Przyczółek Grochowski
+
+
+ 🗺️ Polish Powiaty
+
+

Powiat Ropczycko-Sędziszowski
+

Powiat Łosicki
+

Powiat Piaseczyński
+

Powiat Radomski
+

Powiat Sierpecki
+
+
+ 🏙️ Warsaw Districts
+
+

Śródmieście
+

Mokotów
+

Praga Północ
+

Praga Południe
+

Wola
+
+
+
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())