{
    "version": "https:\/\/jsonfeed.org\/version\/1",
    "title": "M9K blog: заметки с тегом prettymaps",
    "_rss_description": "Заметки обо всём: devops, lifestyle, культура.",
    "_rss_language": "ru",
    "_itunes_email": "",
    "_itunes_categories_xml": "",
    "_itunes_image": "",
    "_itunes_explicit": "",
    "home_page_url": "https:\/\/blog.m9k.space\/tags\/prettymaps\/",
    "feed_url": "https:\/\/blog.m9k.space\/tags\/prettymaps\/json\/",
    "icon": "https:\/\/blog.m9k.space\/user\/userpic@2x.jpg?1769095194",
    "author": {
        "name": "M",
        "url": "https:\/\/blog.m9k.space\/",
        "avatar": "https:\/\/blog.m9k.space\/user\/userpic@2x.jpg?1769095194"
    },
    "items": [
        {
            "id": "238",
            "url": "https:\/\/blog.m9k.space\/all\/prettymaps-2\/",
            "title": "Prettymaps в Docker: генератор картографических постеров",
            "content_html": "<div class=\"e2-text-picture\">\n<img src=\"https:\/\/blog.m9k.space\/pictures\/akademgorodok.png\" width=\"1000\" height=\"1045\" alt=\"\" \/>\n<\/div>\n<p>Поигрался с либой <a href=\"https:\/\/github.com\/marceloprates\/prettymaps\">PrettyMaps<\/a>, которая умеет строить из OSM прикольные постеры.<\/p>\n<p>Собрал контейнер, который позволяет сделать карту.<\/p>\n<p>Сборка без прокси:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">DOCKER_BUILDKIT=1 docker build -t prettymaps-docker-test .<\/code><\/pre><p>Сборка с SOCKS5-прокси:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">DOCKER_BUILDKIT=1 docker build --network=host --build-arg PROXY_SOCKS5=$PROXY_IP:$PROXY_PORT -t prettymaps-docker-test .<\/code><\/pre><p>Запуск:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">docker run --rm \\\r\n  -v &quot;$(pwd)&quot;:\/work \\\r\n  -e LAT=54.8482742 \\\r\n  -e LON=83.0973263 \\\r\n  -e RADIUS_M=1500 \\\r\n  -e TITLE=&quot;Академгородок&quot; \\\r\n  -e OUTPUT_NAME=akadem_coords \\\r\n  prettymaps-docker-test<\/code><\/pre><p><b>Dockerfile<\/b><\/p>\n<pre class=\"e2-text-code\"><code class=\"\"># syntax=docker\/dockerfile:1\r\n#\r\n# Build without proxy:\r\n# DOCKER_BUILDKIT=1 docker build -t prettymaps-docker-test .\r\n#\r\n# Build with SOCKS5 proxy:\r\n# DOCKER_BUILDKIT=1 docker build --network=host \\\r\n#     --build-arg PROXY_SOCKS5=$PROXY_IP:$PROXY_PORT \\\r\n#     -t prettymaps-docker-test .\r\n#\r\n# Do not upgrade pip (newer pip+urllib3 may break SOCKS).\r\n# Pre-install shapely to avoid resolver loops on shapely[vectorized].\r\n\r\n# --------------------------------------------------------------------------\r\n# Stage 1: builder\r\n# --------------------------------------------------------------------------\r\n\r\nFROM debian:bookworm-slim AS builder\r\n\r\nARG PROXY_SOCKS5=&quot;&quot;\r\nARG DEBIAN_FRONTEND=noninteractive\r\n\r\nENV LANG=C.UTF-8 \\\r\n    LC_ALL=C.UTF-8 \\\r\n    PYTHONUNBUFFERED=1 \\\r\n    PIP_DISABLE_PIP_VERSION_CHECK=1 \\\r\n    PIP_DEFAULT_TIMEOUT=120 \\\r\n    PIP_PREFER_BINARY=1\r\n\r\nRUN --mount=type=cache,target=\/var\/cache\/apt,sharing=locked \\\r\n    --mount=type=cache,target=\/var\/lib\/apt,sharing=locked \\\r\n    rm -f \/etc\/apt\/apt.conf.d\/docker-clean &amp;&amp; \\\r\n    apt-get update &amp;&amp; apt-get install -y --no-install-recommends \\\r\n        ca-certificates \\\r\n        curl \\\r\n        python3 \\\r\n        python3-pip \\\r\n        python3-venv \\\r\n        python3-dev \\\r\n        python3-socks \\\r\n        build-essential \\\r\n        gdal-bin \\\r\n        libgdal-dev \\\r\n        libgeos-dev \\\r\n        libproj-dev \\\r\n        libspatialindex-dev \\\r\n        git \\\r\n        fonts-dejavu-core\r\n\r\n# Python environment\r\nRUN python3 -m venv --system-site-packages \/opt\/venv\r\n\r\nENV PATH=&quot;\/opt\/venv\/bin:$PATH&quot;\r\n\r\n# Optional SOCKS5 proxy.\r\n# Empty PROXY_SOCKS5 disables the proxy.\r\n# Proxy settings apply only to the builder stage.\r\n\r\nENV ALL_PROXY=${PROXY_SOCKS5:+socks5h:\/\/}${PROXY_SOCKS5}\r\n\r\nENV HTTP_PROXY=${ALL_PROXY} \\\r\n    HTTPS_PROXY=${ALL_PROXY} \\\r\n    http_proxy=${ALL_PROXY} \\\r\n    https_proxy=${ALL_PROXY} \\\r\n    no_proxy=localhost,127.0.0.1\r\n\r\n# --------------------------------------------------------------------------\r\n# Python dependencies\r\n# --------------------------------------------------------------------------\r\n\r\n# 1. Base tooling, NumPy and Shapely\r\n\r\nRUN --mount=type=cache,target=\/root\/.cache\/pip \\\r\n    pip install \\\r\n        setuptools \\\r\n        wheel \\\r\n        pysocks \\\r\n        &quot;numpy&gt;=1.26.4&quot; \\\r\n        &quot;shapely&gt;=2.0.0&quot;\r\n\r\n# 2. Prettymaps runtime dependencies\r\n# Streamlit and Marimo are intentionally excluded.\r\n\r\nRUN --mount=type=cache,target=\/root\/.cache\/pip \\\r\n    pip install \\\r\n        &quot;matplotlib&gt;=3.9.0&quot; \\\r\n        &quot;osmnx&gt;=2.0.5&quot; \\\r\n        &quot;rasterio&gt;=1.4.3&quot; \\\r\n        &quot;rioxarray&gt;=0.18.2&quot; \\\r\n        &quot;opencv-python-headless&gt;=4.11.0.86&quot; \\\r\n        &quot;scikit-image&gt;=0.25.2&quot; \\\r\n        &quot;thefuzz&gt;=0.22.1&quot; \\\r\n        &quot;elevation&gt;=1.1.3&quot; \\\r\n        &quot;tqdm&gt;=4.70.0&quot; \\\r\n        &quot;scikit-learn&gt;=1.6.1&quot; \\\r\n        &quot;vsketch&gt;=1.0.0&quot; \\\r\n        &quot;ipython&quot; \\\r\n        &quot;ipykernel&gt;=6.29.5&quot;\r\n\r\n# 3. Prettymaps without optional dependencies\r\n\r\nRUN --mount=type=cache,target=\/root\/.cache\/pip \\\r\n    pip install prettymaps --no-deps\r\n\r\n# --------------------------------------------------------------------------\r\n# Stage 2: runtime\r\n# --------------------------------------------------------------------------\r\n\r\nFROM debian:bookworm-slim AS runtime\r\n\r\nARG DEBIAN_FRONTEND=noninteractive\r\n\r\nENV LANG=C.UTF-8 \\\r\n    LC_ALL=C.UTF-8 \\\r\n    PYTHONUNBUFFERED=1 \\\r\n    PATH=&quot;\/opt\/venv\/bin:$PATH&quot;\r\n\r\nRUN --mount=type=cache,target=\/var\/cache\/apt,sharing=locked \\\r\n    --mount=type=cache,target=\/var\/lib\/apt,sharing=locked \\\r\n    rm -f \/etc\/apt\/apt.conf.d\/docker-clean &amp;&amp; \\\r\n    apt-get update &amp;&amp; apt-get install -y --no-install-recommends \\\r\n        ca-certificates \\\r\n        python3 \\\r\n        python3-socks \\\r\n        gdal-bin \\\r\n        libgdal32 \\\r\n        libgeos-c1v5 \\\r\n        libproj25 \\\r\n        libspatialindex6 \\\r\n        fonts-dejavu-core \\\r\n        libgl1 \\\r\n        libglib2.0-0 \\\r\n        libxkbcommon0 \\\r\n        libdbus-1-3 \\\r\n        libxcb-cursor0 \\\r\n        libxcb-xinerama0\r\n\r\n# Copy Python virtual environment\r\nCOPY --from=builder \/opt\/venv \/opt\/venv\r\n\r\n# Application\r\nWORKDIR \/work\r\n\r\nVOLUME \/work\r\n\r\nCOPY generate.py \/opt\/generate.py\r\n\r\n# Prefer user-provided script from \/work.\r\n# Fall back to the bundled script.\r\n\r\nENTRYPOINT [&quot;sh&quot;, &quot;-c&quot;, &quot;script=generate.py; if [ -f \\&quot;\/work\/$script\\&quot; ]; then exec python3 \\&quot;\/work\/$script\\&quot; \\&quot;$@\\&quot;; else exec python3 \\&quot;\/opt\/$script\\&quot; \\&quot;$@\\&quot;; fi&quot;, &quot;--&quot;]\r\n\r\nCMD []<\/code><\/pre><p><b>Скрипт generate.py<\/b><\/p>\n<pre class=\"e2-text-code\"><code class=\"\">#!\/usr\/bin\/env python3\r\n&quot;&quot;&quot;\r\nGenerate a prettymaps poster.\r\n\r\nParameters (priority: CLI args &gt; env vars &gt; defaults):\r\n\r\n  QUERY \/ --query              location string (geocoded by osmnx)\r\n  LAT \/ --lat                  latitude (use with LON instead of QUERY)\r\n  LON \/ --lon                  longitude\r\n  RADIUS_M \/ --radius          radius in metres\r\n  CIRCLE \/ --circle            circular clip (true\/false, 1\/0, yes\/no)\r\n  TITLE \/ --title              map title\r\n  PRESET \/ --preset            prettymaps preset name\r\n  OUTPUT_NAME \/ --output-name  basename for png\/pdf (no extension)\r\n  DPI \/ --dpi                  output dpi\r\n&quot;&quot;&quot;\r\n\r\nimport argparse\r\nimport os\r\nfrom pathlib import Path\r\n\r\nimport osmnx as ox\r\n\r\n# osmnx 2.x: project_gdf left the top-level namespace\r\nif not hasattr(ox, &quot;project_gdf&quot;):\r\n    ox.project_gdf = ox.projection.project_gdf\r\nif not hasattr(ox, &quot;project_graph&quot;):\r\n    if hasattr(ox, &quot;projection&quot;) and hasattr(ox.projection, &quot;project_graph&quot;):\r\n        ox.project_graph = ox.projection.project_graph\r\n\r\nimport prettymaps\r\nimport prettymaps.draw as _pm_draw\r\nimport matplotlib\r\n\r\nmatplotlib.use(&quot;Agg&quot;)\r\n\r\n# --- patch: KeyError 'highway' when streets gdf has no highway column ---\r\n_orig_graph_to_shapely = _pm_draw.graph_to_shapely\r\n\r\n\r\ndef _safe_graph_to_shapely(gdf, width=1.0):\r\n    if gdf is None or getattr(gdf, &quot;empty&quot;, True):\r\n        from shapely.geometry import GeometryCollection\r\n\r\n        return GeometryCollection()\r\n    if isinstance(width, dict) and &quot;highway&quot; not in gdf.columns:\r\n        width = width.get(&quot;default&quot;, next(iter(width.values()), 1.0))\r\n    return _orig_graph_to_shapely(gdf, width)\r\n\r\n\r\n_pm_draw.graph_to_shapely = _safe_graph_to_shapely\r\n# -----------------------------------------------------------------------\r\n\r\n# --- defaults (single place) ---\r\nDEFAULT_QUERY = &quot;Akademgorodok, Novosibirsk, Russia&quot;\r\nDEFAULT_LAT = None\r\nDEFAULT_LON = None\r\nDEFAULT_RADIUS_M = 2200\r\nDEFAULT_CIRCLE = True\r\nDEFAULT_TITLE = &quot;Академгородок · Новосибирск&quot;\r\nDEFAULT_PRESET = &quot;default&quot;\r\nDEFAULT_OUTPUT_NAME = &quot;akademgorodok_poster&quot;\r\nDEFAULT_DPI = 300\r\nDEFAULT_FIGSIZE = (14, 14)\r\n# --------------------------------\r\n\r\n\r\ndef _env(name, default=None):\r\n    val = os.environ.get(name)\r\n    if val is None or val == &quot;&quot;:\r\n        return default\r\n    return val\r\n\r\n\r\ndef _env_bool(name, default):\r\n    val = os.environ.get(name)\r\n    if val is None or val == &quot;&quot;:\r\n        return default\r\n    return val.strip().lower() in (&quot;1&quot;, &quot;true&quot;, &quot;yes&quot;, &quot;y&quot;, &quot;on&quot;)\r\n\r\n\r\ndef _env_float(name, default):\r\n    val = os.environ.get(name)\r\n    if val is None or val == &quot;&quot;:\r\n        return default\r\n    return float(val)\r\n\r\n\r\ndef _env_int(name, default):\r\n    val = os.environ.get(name)\r\n    if val is None or val == &quot;&quot;:\r\n        return default\r\n    return int(val)\r\n\r\n\r\ndef parse_args():\r\n    p = argparse.ArgumentParser(description=&quot;Generate a prettymaps poster&quot;)\r\n    p.add_argument(\r\n        &quot;--query&quot;,\r\n        default=_env(&quot;QUERY&quot;, DEFAULT_QUERY),\r\n        help=&quot;Location string for geocoding&quot;,\r\n    )\r\n    p.add_argument(\r\n        &quot;--lat&quot;,\r\n        type=float,\r\n        default=_env_float(&quot;LAT&quot;, DEFAULT_LAT),\r\n        help=&quot;Latitude (use with --lon instead of --query)&quot;,\r\n    )\r\n    p.add_argument(\r\n        &quot;--lon&quot;,\r\n        type=float,\r\n        default=_env_float(&quot;LON&quot;, DEFAULT_LON),\r\n        help=&quot;Longitude (use with --lat instead of --query)&quot;,\r\n    )\r\n    p.add_argument(\r\n        &quot;--radius&quot;,\r\n        type=int,\r\n        default=_env_int(&quot;RADIUS_M&quot;, DEFAULT_RADIUS_M),\r\n        help=&quot;Radius in metres&quot;,\r\n    )\r\n    p.add_argument(\r\n        &quot;--circle&quot;,\r\n        default=None,\r\n        help=&quot;Circular clip: true\/false (env CIRCLE)&quot;,\r\n    )\r\n    p.add_argument(\r\n        &quot;--title&quot;,\r\n        default=_env(&quot;TITLE&quot;, DEFAULT_TITLE),\r\n        help=&quot;Map title&quot;,\r\n    )\r\n    p.add_argument(\r\n        &quot;--preset&quot;,\r\n        default=_env(&quot;PRESET&quot;, DEFAULT_PRESET),\r\n        help=&quot;prettymaps preset name&quot;,\r\n    )\r\n    p.add_argument(\r\n        &quot;--output-name&quot;,\r\n        default=_env(&quot;OUTPUT_NAME&quot;, DEFAULT_OUTPUT_NAME),\r\n        help=&quot;Output basename without extension&quot;,\r\n    )\r\n    p.add_argument(\r\n        &quot;--dpi&quot;,\r\n        type=int,\r\n        default=_env_int(&quot;DPI&quot;, DEFAULT_DPI),\r\n        help=&quot;Output DPI&quot;,\r\n    )\r\n    args = p.parse_args()\r\n\r\n    if args.circle is None:\r\n        args.circle = _env_bool(&quot;CIRCLE&quot;, DEFAULT_CIRCLE)\r\n    else:\r\n        args.circle = str(args.circle).strip().lower() in (\r\n            &quot;1&quot;,\r\n            &quot;true&quot;,\r\n            &quot;yes&quot;,\r\n            &quot;y&quot;,\r\n            &quot;on&quot;,\r\n        )\r\n\r\n    return args\r\n\r\n\r\ndef main():\r\n    args = parse_args()\r\n\r\n    output_dir = Path(&quot;\/work&quot;) if Path(&quot;\/work&quot;).is_dir() else Path(&quot;.&quot;)\r\n    output_dir.mkdir(parents=True, exist_ok=True)\r\n    out_png = output_dir \/ f&quot;{args.output_name}.png&quot;\r\n    out_pdf = output_dir \/ f&quot;{args.output_name}.pdf&quot;\r\n\r\n    if args.lat is not None and args.lon is not None:\r\n        location = (args.lat, args.lon)\r\n        location_desc = f&quot;({args.lat}, {args.lon})&quot;\r\n    else:\r\n        location = args.query\r\n        location_desc = args.query\r\n\r\n    print(f&quot;Location: {location_desc}&quot;)\r\n    print(f&quot;Radius: {args.radius} m, circle={args.circle}, preset={args.preset}&quot;)\r\n    print(f&quot;Title: {args.title}&quot;)\r\n    print(&quot;Downloading OSM data and rendering (this can take several minutes)...&quot;)\r\n\r\n    plot = prettymaps.plot(\r\n        location,\r\n        radius=args.radius,\r\n        circle=args.circle,\r\n        figsize=DEFAULT_FIGSIZE,\r\n        preset=args.preset,\r\n    )\r\n\r\n    if args.title:\r\n        plot.ax.set_title(\r\n            args.title,\r\n            fontsize=28,\r\n            pad=20,\r\n            fontfamily=&quot;DejaVu Sans&quot;,\r\n        )\r\n\r\n    plot.fig.savefig(\r\n        out_png,\r\n        dpi=args.dpi,\r\n        bbox_inches=&quot;tight&quot;,\r\n        facecolor=plot.fig.get_facecolor(),\r\n        edgecolor=&quot;none&quot;,\r\n    )\r\n    print(f&quot;Saved: {out_png}&quot;)\r\n\r\n    plot.fig.savefig(\r\n        out_pdf,\r\n        bbox_inches=&quot;tight&quot;,\r\n        facecolor=plot.fig.get_facecolor(),\r\n        edgecolor=&quot;none&quot;,\r\n    )\r\n    print(f&quot;Saved: {out_pdf}&quot;)\r\n    print(&quot;Done.&quot;)\r\n\r\n\r\nif __name__ == &quot;__main__&quot;:\r\n    main()<\/code><\/pre>",
            "date_published": "2026-09-18T12:57:51+00:00",
            "date_modified": "2026-09-18T12:58:44+00:00",
            "image": "https:\/\/blog.m9k.space\/pictures\/akademgorodok.png",
            "_date_published_rfc2822": "Fri, 18 Sep 2026 12:57:51 +0000",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "238",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/blog.m9k.space\/pictures\/akademgorodok.png"
                ]
            }
        }
    ],
    "_e2_version": 3877,
    "_e2_ua_string": "E2 (v3877; Aegea)"
}