{"id":50567927,"url":"https://github.com/nimblemarkets/ntcharts-osm","last_synced_at":"2026-06-04T16:01:24.303Z","repository":{"id":357159652,"uuid":"1223033137","full_name":"NimbleMarkets/ntcharts-osm","owner":"NimbleMarkets","description":"NTCharts OpenStreetMap (OSM) component","archived":false,"fork":false,"pushed_at":"2026-05-22T15:24:27.000Z","size":1465,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T20:22:28.481Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NimbleMarkets.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-28T00:22:49.000Z","updated_at":"2026-05-22T15:24:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/NimbleMarkets/ntcharts-osm","commit_stats":null,"previous_names":["nimblemarkets/ntcharts-osm"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/NimbleMarkets/ntcharts-osm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NimbleMarkets%2Fntcharts-osm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NimbleMarkets%2Fntcharts-osm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NimbleMarkets%2Fntcharts-osm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NimbleMarkets%2Fntcharts-osm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NimbleMarkets","download_url":"https://codeload.github.com/NimbleMarkets/ntcharts-osm/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NimbleMarkets%2Fntcharts-osm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33912343,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-04T16:01:21.828Z","updated_at":"2026-06-04T16:01:24.297Z","avatar_url":"https://github.com/NimbleMarkets.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ntcharts-osm — Terminal OpenStreetMap widget for Bubble Tea\n\n\u003cp\u003e\n    \u003ca href=\"https://github.com/NimbleMarkets/ntcharts-osm/tags\"\u003e\u003cimg src=\"https://img.shields.io/github/tag/NimbleMarkets/ntcharts-osm.svg\" alt=\"Latest Release\"\u003e\u003c/a\u003e\n    \u003ca href=\"https://pkg.go.dev/github.com/NimbleMarkets/ntcharts-osm?tab=doc\"\u003e\u003cimg src=\"https://godoc.org/github.com/NimbleMarkets/ntcharts-osm?status.svg\" alt=\"GoDoc\"\u003e\u003c/a\u003e\n    \u003ca href=\"https://github.com/NimbleMarkets/ntcharts-osm/blob/main/CODE_OF_CONDUCT.md\"\u003e\u003cimg src=\"https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg\"  alt=\"Code Of Conduct\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n`ntcharts-osm` is a [Bubble Tea](https://github.com/charmbracelet/bubbletea) widget that renders OpenStreetMap tiles in the terminal. It pairs [`flopp/go-staticmaps`](https://github.com/flopp/go-staticmaps) for tile fetching with [`ntcharts/v2/picture`](https://github.com/NimbleMarkets/ntcharts) for image rendering — half-block glyphs anywhere, full-resolution Kitty graphics on terminals that support them (Kitty, Ghostty, WezTerm).\n\n\u003cp align=\"center\"\u003e\u003ca href=\"https://nimblemarkets.github.io/ntcharts-osm\"/\u003e\u003cb\u003eLive Demo\u003c/b\u003e\u003ca\u003e (\u003ca href=\"./examples/mapview/main.go\"\u003esource\u003c/a\u003e)\u003cbr\u003e\u003ca href=\"https://nimblemarkets.github.io/ntcharts-osm\"/\u003e\u003cimg src=\"examples/mapview/demo.gif\" alt=\"quickstart gif\" width=\"75%\"/\u003e\u003c/a\u003e\u003c/p\u003e\n\n## Quickstart\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"os\"\n\n    tea \"charm.land/bubbletea/v2\"\n    \"github.com/NimbleMarkets/ntcharts-osm/mapview\"\n)\n\ntype model struct{ mv mapview.Model }\n\nfunc (m model) Init() tea.Cmd { return m.mv.Init() }\n\nfunc (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n    if k, ok := msg.(tea.KeyMsg); ok \u0026\u0026 (k.String() == \"q\" || k.String() == \"ctrl+c\") {\n        return m, tea.Quit\n    }\n    if sz, ok := msg.(tea.WindowSizeMsg); ok {\n        m.mv.SetSize(sz.Width, sz.Height)\n    }\n    var cmd tea.Cmd\n    m.mv, cmd = m.mv.Update(msg)\n    return m, cmd\n}\n\nfunc (m model) View() tea.View { return m.mv.View() }\n\nfunc main() {\n    mv := mapview.New(0, 0)\n    mv.SetLatLng(40.6892, -74.0445, 13) // Statue of Liberty\n    if _, err := tea.NewProgram(model{mv: mv}).Run(); err != nil {\n        fmt.Println(err); os.Exit(1)\n    }\n}\n```\n\nPan with arrows or `hjkl`, zoom with `+`/`-`. The widget owns those bindings — no parent wiring needed.\n\n## Demo\n\nA fuller single-pane demo lives at [`examples/mapview`](./examples/mapview/main.go) — adds tile-style cycling and Glyph/Kitty mode toggling.\n\n```sh\ntask build-ex-mapview\n./bin/ntcharts-osm-mapview\n```\n\n## Tile styles\n\n`mapview.SetStyle(...)` switches between nine tile providers from [`flopp/go-staticmaps`](https://github.com/flopp/go-staticmaps): `Wikimedia`, `OpenStreetMaps`, `OpenTopoMap`, `OpenCycleMap`, `CartoLight`, `CartoDark`, `StamenToner`, `StamenTerrain`, `ArcgisWorldImagery`. Each comes with the upstream provider's terms-of-use and attribution requirements — read them before shipping a public app.\n\n## Render modes\n\n| Mode | What it does | Where it works |\n|---|---|---|\n| `mapview.RenderGlyph` (default) | Half-block ANSI from [`pixterm/ansimage`](https://github.com/eliukblau/pixterm), via `ntcharts/v2/picture` | Any modern terminal |\n| `mapview.RenderKitty` | Full-resolution image via Kitty graphics protocol | Kitty, Ghostty, WezTerm |\n\n`mv.SetRenderMode(mode)` returns a `tea.Cmd` that re-renders at the new mode. Toggling away from Kitty automatically deletes the uploaded image so no ghost stays in the terminal.\n\n## Bubble Tea version\n\nTargets Bubble Tea **v2** (`charm.land/bubbletea/v2`). No v1 backport.\n\n## Routing messages across multiple picture owners\n\n`mapview.IsMapUpdate(msg)` is a **\"may need this\"** predicate, not an ownership predicate. It returns `true` for both mapview's own messages *and* for every `picture.IsPictureMsg` (Kitty frames, cell-size events, capability probes). Picture messages are program-wide — they belong to the runtime, not to any one model.\n\nIf your program embeds `picture.Model` in more than one place (say, a `mapview.Model` *and* a `pictureurl.Model` rendering a product image), do NOT route picture messages with `else if` — broadcast them to every picture owner, otherwise one of them will starve:\n\n```go\nmapMsg := mapview.IsMapUpdate(msg)\nprodMsg := pictureurl.IsPictureMsg(msg)\n\nif mapMsg || prodMsg {\n    var cmds []tea.Cmd\n    if mapMsg {\n        var cmd tea.Cmd\n        m.mv, cmd = m.mv.Update(msg)\n        cmds = append(cmds, cmd)\n    }\n    if prodMsg {\n        var cmd tea.Cmd\n        m.prod, cmd = m.prod.Update(msg)\n        cmds = append(cmds, cmd)\n    }\n    return m, tea.Batch(cmds...)\n}\n```\n\nFor the narrower \"is this message owned by mapview alone\" predicate (the `MapCoordinates` input, async tile-render results, debounce ticks), use `mapview.IsMapOwnMsg(msg)`. Each model's own `Update` already filters by instance ID where applicable, so broadcasting picture messages is safe.\n\n## Known caveats\n\n- **Tile fetching is synchronous per render.** Each render builds its own `*sm.Context` inside the dispatched goroutine and tags the result with a generation counter, so rapid pan / zoom / resize fires safely-parallel renders and stale results are dropped. Composited images are kept in a small per-Model LRU keyed on `(lat, lng, zoom, cols, rows, style, oversample, markers)` — revisiting a state hits the cache synchronously (no goroutine, no Loading overlay). Default cap is 16 entries; tune via `mapview.NewWithConfig(Config{CacheCap: N})` (`-1` disables caching).\n- **Server-side supersample (`Config.Oversample`).** Raises the source-image pixel density without changing visible geographic coverage — `Oversample: N` (powers of 2) renders the same area at `N×` per-cell resolution and `+log2(N)` OSM tile zoom, so Kitty terminals can downscale a sharper source. `1` (default) keeps current behavior; `2` is a noticeable boost; `4` is hi-DPI quality at ~16× the tile fetches. Capped so the effective tile zoom never exceeds 19. Glyph mode pays the cost without visible benefit.\n- **Client-side optical zoom (`Config.OpticalZoom` / `mv.SetOpticalZoom(n)`).** Magnifies the *cached* source image by cropping the center `1/2^n` of each axis and letting the renderer scale it back up to the cell rectangle. No network, no tile-render goroutine — switching is instant. Pixelated at high N (it's digital zoom in spirit even though we call it optical), but useful for going past the OSM tile-zoom ceiling or just inspecting fine detail without re-fetching. Composes with `Oversample`: e.g. `Oversample: 2` + `OpticalZoom: 1` is a 2× zoomed view rendered from a 2× supersampled source.\n- **Aspect-ratio guardrail (`Config.MaxAspectRatio` / `Config.LetterboxColor`).** Maps display correctly at any cell-rect AR — every pixel sits at its true geographic location — but at extreme ARs (a 100×5 status bar, say) viewers find the result disorienting because their mental model of \"a map\" is roughly square. `MaxAspectRatio: 3.0` lets the cell rect's AR range up to 3:1 in either direction; beyond that, the map portion is rendered at the boundary AR centered in the cell rect, with the rest filled by `LetterboxColor` (default opaque black; `color.Transparent` shows the terminal background through the bars). The letterbox is composed into the source image *before* picture renders, so Glyph and Kitty show identical letterbox geometry — toggling render modes doesn't change layout. Default `0` keeps the current \"fill the cell rect at any AR\" behavior; opt in when your layout might land in extreme territory.\n- **No built-in API key handling.** Tile providers that require a key (e.g. Thunderforest) are not bundled — add a custom `tileProvider` if you need one.\n- **Geocoding uses Nominatim** with no caching. Respect their [usage policy](https://operations.osmfoundation.org/policies/nominatim/) for production traffic.\n\n## License\n\nMany thanks to the **OpenStreetMap Foundation** ([donate](https://supporting.openstreetmap.org/donate/)).\n\n[MIT License](./LICENSE.txt) — Copyright (c) 2026 [Neomantra Corp](https://www.neomantra.com).\n\n----\nMade with :heart: and :fire: by the team behind [Nimble.Markets](https://nimble.markets).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnimblemarkets%2Fntcharts-osm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnimblemarkets%2Fntcharts-osm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnimblemarkets%2Fntcharts-osm/lists"}