maposter 0.2.0
maposter: ^0.2.0 copied to clipboard
Turn a city name into a minimalist OpenStreetMap poster: geocoding, Overpass map data, Mercator projection, CustomPainter rendering, and PNG/PDF export.
πΊοΈ maposter #
Turn any city in the world into a beautiful, minimalist map poster β straight from OpenStreetMap data.
maposter is a framework-agnostic Flutter package that takes a city name and produces a print-ready map poster. It geocodes the place, pulls roads, water and parks from OpenStreetMap, projects them with Web Mercator, renders them onto a Canvas, and exports to high-resolution PNG or PDF. Seventeen hand-tuned themes are bundled β and you can add your own in a single line.
No state-management or storage dependency is forced on you, and caching is fully pluggable.
πΌοΈ Gallery #
Real posters generated with maposter β each in a different bundled theme:
![]() Kolkata Β· monochrome_blue |
![]() Dubai Β· midnight_blue |
![]() Bangalore Β· forest |
![]() Alexandria Β· autumn |
β¨ Features #
- π Geocoding β city + country β coordinates via OpenStreetMap Nominatim, rate-limited per policy.
- π£οΈ Map data β roads, water bodies and parks fetched from the Overpass API and parsed off the main thread in an isolate.
- π Web Mercator projection β accurate lat/lon β screen math, with aspect-ratio-aware framing.
- π¨ 17 bundled themes β from
noirtoneon_cyberpunkβ plus first-class custom themes. - πΌοΈ Canvas rendering β a ready-to-use
CustomPainteryou can drop into any widget tree. - π€ PNG & PDF export β print-ready output at configurable DPI, with a built-in memory guard.
- π§© Framework-agnostic β plain Dart API + a single
MaposterEnginefacade. Works with Riverpod, BLoC, Provider, or nothing at all. - πΎ Pluggable caching β ships with an in-memory cache; bring your own (Hive, sqflite, filesβ¦) via one interface.
- π Cancellable requests β pass a
CancelTokento abort in-flight map fetches. - π₯οΈ Every platform β Android, iOS, web, macOS, Windows and Linux.
π¦ Installation #
flutter pub add maposter
Or add it to your pubspec.yaml (see the latest version):
dependencies:
maposter: <latest-version>
π Quick start #
import 'package:flutter/widgets.dart';
import 'package:maposter/maposter.dart';
final engine = MaposterEngine(
// userAgent is REQUIRED β OpenStreetMap's Nominatim policy needs a real,
// identifying agent (your app name + a contact).
const MaposterConfig(userAgent: 'myapp/1.0 (com.example.myapp)'),
);
// 1. City name β coordinates
final coords = await engine.geocode('Kolkata', 'India');
// 2. Coordinates β map geometry (roads, water, parks)
final data = await engine.fetchMapData((coords.latitude, coords.longitude));
// 3. Pick a theme
final theme = await engine.getTheme('noir');
// 4. Build the painter
final painter = engine.buildPainter(
mapData: data,
theme: theme,
cityName: 'Kolkata',
countryName: 'India',
latitude: coords.latitude,
longitude: coords.longitude,
);
// 5a. Render it live in a widget tree:
// CustomPaint(painter: painter)
// 5b. β¦or export it:
final png = await engine.exportPng(painter, const Size(1200, 1600));
final pdf = await engine.exportPdf(png, const Size(1200, 1600));
π§ Step by step #
Configure the engine #
MaposterConfig carries everything the engine needs to talk to OpenStreetMap. Only userAgent is required:
final engine = MaposterEngine(
const MaposterConfig(
userAgent: 'myapp/1.0 (com.example.myapp)',
defaultRadiusMeters: 18000, // how much of the city to capture
),
);
Geocode a place #
final coords = await engine.geocode('Paris', 'France');
print('${coords.displayName} @ ${coords.latitude}, ${coords.longitude}');
Fetch map data #
final data = await engine.fetchMapData(
(coords.latitude, coords.longitude),
radiusMeters: 20000, // optional; defaults to config.defaultRadiusMeters
);
print('${data.roads.length} roads, '
'${data.waterFeatures.length} water, '
'${data.parkFeatures.length} parks');
Render & export #
buildPainter returns a MaposterPainter (a CustomPainter). Render it live with CustomPaint, or rasterize it:
// 3Γ scale β 300 DPI for a 1200Γ1600 poster
final png = await engine.exportPng(painter, const Size(1200, 1600), scale: 3.0);
final pdf = await engine.exportPdf(png, const Size(1200, 1600));
π¨ Themes #
Seventeen themes ship with the package:
noir |
blueprint |
autumn |
emerald |
forest |
ocean |
sunset |
terracotta |
warm_beige |
midnight_blue |
monochrome_blue |
neon_cyberpunk |
japanese_ink |
pastel_dream |
copper_patina |
gradient_roads |
contrast_zones |
final all = await engine.getAllThemes(); // List<MapTheme>
final noir = await engine.getTheme('noir');
Custom themes #
Define your own in code with hex strings β they merge with the built-ins. Reuse a built-in id to override it:
final engine = MaposterEngine(
config,
customThemes: [
MapTheme.fromHex(
id: 'brand',
name: 'Brand',
bg: '#0A0A0A',
text: '#E0FF00',
gradientColor: '#0A0A0A',
water: '#111111',
parks: '#1A1A1A',
roadMotorway: '#E0FF00',
roadPrimary: '#C8E000',
roadSecondary: '#96A800',
roadTertiary: '#647000',
roadResidential: '#323800',
roadDefault: '#647000',
),
],
);
final mine = await engine.getTheme('brand'); // your theme
πΎ Caching #
The engine caches geocoding and map-data results through a CacheStore. By default it uses an in-memory cache (InMemoryCacheStore) that lasts for the life of the process. Provide your own to persist results across launches and avoid re-hitting the network:
import 'package:hive_flutter/hive_flutter.dart';
import 'package:maposter/maposter.dart';
class HiveCacheStore implements CacheStore {
HiveCacheStore(this._box);
final Box<dynamic> _box;
@override
Future<String?> read(String key) async => _box.get(key) as String?;
@override
Future<void> write(String key, String value) => _box.put(key, value);
}
// β¦after opening the box:
final engine = MaposterEngine(config, cache: HiveCacheStore(myBox));
Want to disable caching entirely? Use NoopCacheStore().
βοΈ Configuration reference #
MaposterConfig options:
| Option | Default | Description |
|---|---|---|
userAgent |
required | Identifying UA sent to Nominatim (its policy requires a real one). |
defaultRadiusMeters |
18000 |
Default capture radius when none is passed to fetchMapData. |
nominatimBaseUrl |
https://nominatim.openstreetmap.org |
Geocoding endpoint. |
overpassBaseUrl |
https://overpass-api.de/api/interpreter |
Map-data endpoint. |
nominatimMinGap |
1100 ms |
Minimum gap between geocoding requests. |
overpassMinGap |
2 s |
Minimum gap between Overpass requests. |
connectTimeout |
30 s |
Connection timeout. |
receiveTimeout |
30 s |
Receive timeout (Nominatim). |
overpassReceiveTimeout |
60 s |
Receive timeout for Overpass (queries can be slow). |
π Cancellation #
fetchMapData accepts a CancelToken (re-exported from dio) so you can abort an in-flight request β e.g. when the user starts a new search:
final token = CancelToken();
final future = engine.fetchMapData(center, token: token);
// β¦later:
token.cancel();
β³ Progress reporting #
The first fetch for a city runs several slow, rate-limited Overpass queries, so
a bare spinner can feel stuck. Pass onProgress to fetchMapData to drive a
staged progress UI β it fires as each stage (roads β water β parks) begins, and
flags stages served from cache (which resolve instantly):
await engine.fetchMapData(
center,
onProgress: (p) {
// p.stage is MapDataStage.roads | .water | .parks
// p.index / p.total β e.g. 2 of 3; p.fromCache β instant?
print('${p.stage.name} (${p.index + 1}/${p.total})');
},
);
See example/ for a labeled progress bar built on this.
π§― Error handling #
The engine throws a sealed AppException. Switch over its subtypes for precise handling, or just show message:
try {
final coords = await engine.geocode(city, country);
} on GeocodingException catch (e) {
// no match for that place
} on NetworkException catch (e) {
// e.statusCode is available
} on AppException catch (e) {
showError(e.message);
}
Subtypes: NetworkException, GeocodingException, OverpassException, CacheException, RenderException, AssetException.
π₯οΈ Platform support #
| Android | iOS | Web | macOS | Windows | Linux |
|---|---|---|---|---|---|
| β | β | β | β | β | β |
π± Example app #
A complete demo app β city input, theme picker, live preview and an export sheet (save to gallery / share PNG / share PDF) β lives in example/. It shows how to wire maposter into a real app (here with Riverpod + Hive-backed caching).
πΊοΈ Attribution & usage policy #
Map data is Β© OpenStreetMap contributors, available under the Open Database License (ODbL). The painter already renders an βΒ© OpenStreetMap contributorsβ credit onto every poster β please keep it visible.
This package calls the public Nominatim and Overpass endpoints, which enforce usage policies (rate limits and a required, identifying User-Agent). Always set a real userAgent in MaposterConfig. For heavy or production traffic, point nominatimBaseUrl / overpassBaseUrl at your own hosted instances.
π€ Contributing #
Issues and pull requests are welcome over on GitHub. If you build something with maposter, I'd love to see it!
π License #
MIT Β© Sitam Sardar



