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