Skip to content
Agentmart
Submit a tool

portolan-thumbnails

Generate framed, checked thumbnails from Portolan collections using chiitiler (MapLibre GL Native).

Skill by portolan-sdi

IndexedApache-2.0★ 11
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/portolan-sdi-portolan-thumbnails

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Generate framed, checked thumbnails from Portolan collections using chiitiler (MapLibre GL Native). Renders the collection's default style server-side over the PMTiles the collection links to, with an optional basemap, frames every bbox to the browser card's 3:2 shape, accepts an image only after an automated blank probe and a visual review, then refreshes file:size and file:checksum with portolan check and its fix flag. Requires Node.js 24.12 or newer.

Use cases: backend

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found
Runs as
Python package

Network hosts it talks to 4

  • codeload.github.com · found in install command
  • github.com · found in code
  • services.arcgisonline.com · found in code
  • tile.openstreetmap.org · found in code

Files it touches

  • ~/.claude/skills/portolan-thumbnails

What's inside

Skill name
portolan-thumbnails
Description
Generate framed, checked thumbnails from Portolan collections using chiitiler (MapLibre GL Native). Renders the collection's default style server-side over the PMTiles the collection links to, with an optional basemap, frames every bbox to the browser card's 3:2 shape, accepts an image only after an automated blank probe and a visual review, then refreshes file:size and file:checksum with portolan check and its fix flag. Requires Node.js 24.12 or newer.
License
Apache-2.0
Instructions
389 lines in SKILL.md · view on GitHub

Before installing

  • Ships executable scripts
  • Instructions call the network

Files (7)

  • SKILL.md
  • buildstyle.py · scripts/
  • frame.py · scripts/
  • pick_basemap.sh · scripts/
  • read_collection.py · scripts/
  • render_one.sh · scripts/
  • start_server.sh · scripts/

SKILL.md (first lines)

<!-- drift: depends-on: portolan-cli, portolan-spec -->

# Framed Thumbnails with Chiitiler

Render a collection's default style server-side with
[chiitiler](https://github.com/Kanahiro/chiitiler), frame it to the shape of the
browser card, and check the result before you publish it. The renderer is the
easy half.
Most bad thumbnails come from the bbox.

Use this for any collection whose thumbnail people will look at. The thumbnails
that `portolan add` and `portolan check --fix` draw are the baseline. This replaces
them with an image that matches the portal. The scripts live in `scripts/` beside
this file. Set `SKILL` to this skill's directory once per session.

## Requirements

- Node.js 24.12 or newer, and npm. chiitiler imports `node:sqlite`, so an older
  Node fails at startup with `ERR_UNKNOWN_BUILTIN_MODULE`.
- Git, to clone chiitiler.
- A local checkout of the collection with a `rel: pmtiles` link and a style asset.
- An agent that can view images. Step 5 is not optional.
- DuckDB, optional. Needed only to find a dense subregion in a large collection.

The scripts run on Linux and macOS. They use `nohup` in place of `setsid`, and
fall back from `stat -c%s` and `sha256sum` to `stat -f%z` and `shasum -a 256`.

## Why Framing Is the Whole Job

chiitiler's `/clip` endpoint takes `bbox`, `size`, `quality`, and a style. The
aspect ratio and the zoom both fall out of the bbox, so every framing decision is
a decision about which bbox to send. The raw extent produces tall rectangles in
wide cards, dense layers rendered so far out that tile thinning shows as holes,
and a row of cards that all look alike. Rewriting the bbox reframes the map. It
keeps the aspect ratio of every feature.
`specs/best-practices/conversion-defaults.md`
describes the CLI's own thumbnail as guidance, not conformance.

## Step 1: Read the collection

`read_collection.py` reads the signals from `collection.json` by role and by link
relation, never by file name. Styles are assets with the `style` role, and the
default one also carries `default` (PORTO-CORE-069, PORTO-CORE-070). The PMTiles
is a `rel: pmtiles` link with `pmtiles:layers` (PORTO-FMT-011).

```bash
python3 "$SKILL/scripts/read_collection.py" publico_arbolado/
# {"bbox": "-60.63,-33.92,-60.51,-33.86", "style": ".../styles/default.json",
#  "pmtiles": ".../publico_arbolado.pmtiles", "pmtiles_layers": ["publico_arbolado"],
#  "thumbnail": ".../publico_arbolado.thumb.jpg", "thumbnail_type": "image/jpeg",
#  "geometry_type": "Point", "feature_count": 43525}
```

`bbox` is the starting frame. `thumbnail` and `thumbnail_type` say where to write
and in which format. `pmtiles_layers` is ground truth for `source-layer`.
`feature_count` is the strongest strategy signal, and points and lines need more
zoom than polygons. portolan-cli writes those last two as `geoparquet:*`
properties. The spec defines none of them, and it defines neither
`pmtiles:max_zoom` nor `pmtiles:center`. A catalog from another tool lacks them.
Read the zoom range and
centre from the PMTiles header instead, which always works.

```bash
python3 "$SKILL/scripts/frame.py" \
    --pmtiles publico_arbolado/publico_arbolado.pmtiles
# {"min_zoom": 0, "max_zoom": 13, "center": [-60.578613, -33.888655, 13],
#  "bounds": [-60.638266, -33.923181, -60.512274, -33.860783]}
```

Treat `center` as a hint. tippecanoe sometimes writes a bbox corner rather than a
dense cluster. Confirm it with a feature count before you build a window around it.

## Step 2: Choose a strategy

**A, full extent.** Frame the whole bbox. Right for boundaries, districts, wards,
neighborhoods, watersheds, city limits, and anything with a small feature count.

**B, zoomed window.** A 3:2 window at a chosen zoom, centred on a dense cluster.
Right for parcels, buildings, addresses, service requests, permits, and trees.

These are defaults. Override them when the data says otherwise.

| Signal | Default |
|---|---|
| Polygons, `feature_count` <= 50 | A |
| Name matches boundary, border, limit, district, ward, precinct, zone, tract, county, city, watershed, region, and geometry is polygons | A |
| Any geometry, `feature_count` >= 5000 | B |
| Points or lines, `feature_count` >= 1000 | B |
| Anything else | A when `fill` >= 0.4, otherwise B |
| `aspect` outside 2.2:1 after capping | Decide explicitly and record why |

`fill` and `aspect` come from Step 3. The last two rows mean you run `frame.py`
first and then reconsider.

## Step 3: Compute the bbox

`frame.py` reshapes a bbox to 3:2 in Web Mercator and reports `fill`, `aspect`,
and the effective zoom.

| Knob | Default | Why |
|---|---|---|
| `TARGET_ASPECT` | 1.5 | The browser card is 350 to about 700 px wide and 250 px tall |
| `MARGIN` | 0.05 | Fractional per axis, so the aspect is unchanged |
| `MAX_CONTEXT` | 2.5 | The short axis grows to at most 2.5x the data span, so data stays at 40% of the frame or more |
| `FRAME_ASPECT_LIMIT` | 2.2 | A capped frame still worse than 2.2:1 means full-extent framing failed |
| `--size` | 1024 | Twice the widest realistic card |

### Strategy A

```bash
BBOX=$(python3 "$SKILL/scripts/read_collection.py" publico_barrios/ \
    | python3 -c "import json,sys; print(json.load(sys.stdin)['bbox'])")
python3 "$SKILL/scripts/frame.py" --bbox "$BBOX" --size 1024
# {"bbox": "-60.651220,-33.935211,-60.495382,-33.848971", "aspect": 1.5,
#  "zoom": 13.17, "span_m": 17347.9, "warnings": [], "fill": 0.957}
```

Read `fill` and `aspect` before you render. A `fill` near 1 means the data nearly
fills the frame. A `fill` of exactly 0.4 means `MAX_CONTEXT` capped the growth and

From portolan-sdi/portolan-skills, Apache-2.0.

More from portolan-sdi/portolan-skills

Signals

Stars
11
Forks
3
Open issues
5
Last commit (this folder)
Oct 6, 2026 (22 days ago)
Last release
Oct 6, 2026 (22 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
Apache-2.0
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from portolan-sdi

Cite this page

portolan-thumbnails skill. Agent Mart. https://agentmart.dev/skills/portolan-sdi-portolan-thumbnails (last updated 2026-10-24)