flutter_map_vector_tiles 2.2.0
flutter_map_vector_tiles: ^2.2.0 copied to clipboard
Vector tiles for flutter_map: a clean, self-contained MapLibre-style vector tile layer with isolate-based decoding, raster tile caching and screen-space label collision.
Changelog #
2.2.0 #
Mapbox-hosted and header-authenticated styles now load, over a broad correctness and performance pass.
- ✨
StyleReadernow actually resolvesmapbox://URIs — style ids, sprite bases and bare tileset ids expand to theirapi.mapbox.comequivalents withapiKeyas the access token. The capability was documented but never implemented. - ✨
StyleReaderacceptsheaders, sent with the style, TileJSON and sprite requests and forwarded to the created tile providers — style loading now works end-to-end against header-authenticated services. - ⚡ MVT tile decoding is about twice as fast (measured 2.0–2.3× on
real tiles): geometry vertices accumulate into a reusable typed-data
scratch buffer instead of a growable
List<double>, which boxed every coordinate on the VM. - ⚡ PMTiles tile blobs are no longer gzip-inflated on the UI isolate on native platforms: the worker isolate inflates them just before decoding (0.5–3 ms per tile that used to compete with the frame budget while panning on a cold cache), and the disk cache now stores the smaller compressed form. As a side effect, servers that send gzip-compressed MVT without declaring it now decode instead of failing. Feature properties are also filtered during decoding instead of being fully materialized and then trimmed.
- ⚡ Tile bytes travel lighter: downloaded tiles are no longer copied a second time before decoding, tiles a source has said don't exist (oceans, sparse zooms) are remembered on disk instead of being re-requested every app session, the offline fallback reads the disk once instead of probing existence first, and PMTiles leaf-directory caching is bounded by bytes rather than entry count alone.
- ⚡ The style engine does far less work per evaluation: zoom-only
paint properties (pure zoom ramps — the most common shape) are
detected at compile time and memoized against the zoom, so a tile
pass evaluates each once instead of per feature; parsed CSS colours
are cached; legacy
infilters use set lookups instead of linear scans; literalget/haskeys read the property map directly; operator dispatch moved from evaluation to parse time; exponential ramps precompute their per-intervalpowfactors; and legacy{token}templates are split once instead of regex-replaced per feature. - ⚡ Less per-frame and per-feature work: zoom-only line layers resolve their stroke style once per tile instead of per feature, circle paints are built per feature instead of per point, literal list properties (fonts, offsets, dash arrays) are converted once at parse time, each symbol's text style is re-evaluated only when the zoom changes instead of every frame, the label collision index no longer allocates an iterator per collision box, curve-safety of label text is computed once per symbol instead of re-scanned per frame, the label pass sorts the per-frame symbol list in place instead of copying it first, and grid maintenance (visible-key enumeration and sorting) is skipped on the ~95% of gesture frames where the integer tile bounds are unchanged.
- 🐛 Coalesced tile loads no longer inherit the first requester's cancellation: a display tile disposed mid-flight (panned away, zoomed past) used to cancel the shared load and leave every other tile waiting on it permanently blank until recreated. Loads now poll a token joined over all waiters — cancelled only when nobody is left — in both tile stores and both network providers.
- 🐛 Tiles whose source failed transiently (network loss, 5xx) now retry: visible tiles re-request missing sources a bounded number of times, just past the stores' failure throttle, instead of staying blank or partial until the zoom level changes. Already-rendered content is not re-rasterized when a retry recovers nothing.
- 🐛 A raster tile load could return no image despite succeeding when the decoded image was evicted by a concurrent load in the same instant; callers now receive a handle that cannot be evicted out from under them.
- 🐛 On devices where isolate spawning fails, the decode pipeline now falls back to the event loop permanently instead of hanging every tile enqueued after the first batch; a decode isolate killed by the OS no longer strands its tile and shrinks the pool — it is replaced and the tile retried.
- 🐛 Turning
showLabelson now lays out labels on the tiles already on screen (they used to appear only on tiles loaded afterwards), and changingspritesre-rasterizes live tiles so fill/line patterns from the old atlas don't linger. Neither refresh refetches or re-fades anything. - 🐛 MVT decoding: a packed field ending in a continuation byte now
fails as a decode error instead of silently reading past the field —
which could render corrupt geometry — or crashing at the buffer end;
and negative
int_valueproperties (10-byte varints) decode to the correct negative number on web instead of a garbage positive one. - 🐛 The label painter's text and glyph caches now dispose their
TextPainters on eviction and teardown instead of leaking the native paragraph memory (flagged under Flutter leak tracking; panning across label-dense areas grew it unboundedly). - 🐛 Legacy
intervalfunctions evaluated exactly on a middle stop returned the previous band's output (wrong style values at every integer zoom boundary); they now select the greatest stop ≤ input, matching MapLibre. - 🐛 Legacy
identityfunctions now return the feature property's raw value; they — like every stop-less legacy function — used to be silently compiled to a constant raw JSON map, making typed paint properties fall back to their spec defaults with no warning. Uncompilable function shapes now degrade with a parser warning. - 🐛 A tile property carrying
Infinityno longer crashes the layer when stringified into atext-fieldtoken. - 🐛
MemoryVectorTileProviderno longer defaults every instance to the samecacheKey: two providers bundling different regions shared decoded-tile and disk cache entries, rendering one region's tiles inside the other. The default key is now derived from the tile set (deterministic, so identical data still shares); pass an explicitcacheKeyto control sharing. Disk entries cached under the old constant key are re-fetched once. - 🐛
VectorTileLayer.clearMemoryCacheduring an open map no longer orphans the caches that in-flight loads keep refilling — refilled entries used to be unreachable by every later clear (a boundedui.Imageleak, worst on exactly the memory-pressure path the call serves).memoryCacheMaxBytesnow actually applies when the shared cache for a source already exists (latest layer wins, including viadidUpdateWidget); it used to be fixed by whichever layer mounted first in the process. - 📦 pub.dev topics retagged for discoverability:
mapreplacesmaps(the topic page the flutter_map ecosystem actually uses) andpmtilesreplacesmvt.
2.1.1 #
Style attribution and MVT decode performance.
- ✨ Style attribution:
Style.attributionsexposes what the style's sources (or the TileJSON they point at) declare, deduplicated and in document order. Attribution is HTML by convention, so eachStyleAttributioncarries the flattenedtextfor a plainTextwidget and thespans— text plus link — if you want it tappable. A source's ownattributionoverrides its TileJSON's, as in MapLibre. The example app now shows this instead of a hardcoded string. - ⚡ Zigzag decoding is branchless again, recovering the ~5% of whole-tile decode time that 2.0.0's web-safe rewrite spent on mispredicted branches. Output is unchanged and still bit-identical between native and web; a latent wrong-sign overflow at the int64 maximum is fixed as a side effect.
2.1.0 #
PMTiles support: Protomaps-style single-file tile archives now work.
- ✨ PMTiles v3 archives: styles with
pmtiles://https://…/planet.pmtilessource URLs load out of the box, andPmTilesVectorTileProvider.open(url)serves any archive directly. Tiles come from HTTP range requests — the header and root directory are fetched once, leaf directories are LRU-cached, and each tile costs at most two range requests. Works on all six platforms; on web the archive host must allow ranged CORS requests, and gunzip uses the browser's nativeDecompressionStream. Archives with brotli or zstd internal compression are rejected with a clear error (gzip and uncompressed are supported — Protomaps builds ship gzip). - The source's
minzoom/maxzoomin the style override the archive header, matching the behaviour of TileJSON sources.
2.0.1 #
Real-world style compatibility: ArcGIS/Esri root.json styles now work.
- 🐛 Relative tile URL templates from TileJSON documents now resolve
against the TileJSON URL instead of the style URL. ArcGIS vector tile
services (
…/VectorTileServer/resources/styles/root.json) declare their source as"url": "../../"with a relativetile/{z}/{y}/{x}.pbftemplate, which previously produced 404s for every tile. - 🐛 Relative tile templates no longer break on
{z}/{x}/{y}placeholders: URL resolution percent-encoded the braces to%7Bz%7D, defeating placeholder substitution. - 🐛
{key}is now substituted in TileJSON source URLs ("url": "https://…/tiles.json?key={key}"), not only in the style URL and the tile templates. - 📚 The README provider table now lists ArcGIS/Esri and self-hosted
servers as verified (Esri
World_Basemap_v2, MapLibre demo tiles) and documents that Protomapspmtiles://archives are not supported.
2.0.0 #
The layer now runs on Flutter web.
- 🌐 Web support: rendering, styles, labels and raster sources work
identically in the browser (CanvasKit/Skwasm renderer — the default
since Flutter 3.29; the removed HTML renderer lacks
Picture.toImageSync). Tile decoding runs on a yielding event-loop queue instead of worker isolates (concurrencyis ignored), and there is no persistent cache on web:cachePath,diskCacheTtl,diskCacheMaximumSizeInBytesandStyleReader(cache: …)are no-ops there — tiles and the style bundle rely on the in-memory caches plus the browser's own HTTP cache, so the disk-backed offline behaviour remains native-only. Style, TileJSON, sprite and tile hosts must sendAccess-Control-Allow-Origin(MapTiler, OpenFreeMap and Stadia do). - 💥 Breaking: the
cacheFolderparameter (Future<Directory> Function()?) onVectorTileLayerandStyleReaderis nowcachePath(Future<String> Function()?), so the public API no longer carries adart:iotype. Migration:cacheFolder: () async => dirbecomescachePath: () async => dir.path; if you never passedcacheFolder, nothing changes — cache locations and defaults are identical, and existing on-disk caches are reused as-is. - 🐛 MVT varint and zigzag decoding no longer relies on 64-bit bitwise operations, which JavaScript truncates to 32 bits — negative coordinate deltas decoded as huge positive values on web. Native results are bit-identical.
1.2.0 #
Two style features that used to be skipped now render.
- 🎨
line-pattern: the sprite is stamped along the line, rotated to the local direction and scaled so its height matches the line width (MapLibre semantics, data-driven patterns included). Per spec the pattern replacesline-colorandline-dasharray;line-opacitystill applies. A missing sprite falls back to the color stroke. - 🛰️ Raster sources inside vector styles (satellite/hybrid imagery,
pre-rendered hillshade tiles):
rasterlayers draw their tiles at the correct position in the layer order.raster-opacity,raster-brightness-min/-max,raster-contrast,raster-saturationandraster-hue-rotatereplicate MapLibre's fragment-shader math (including its factor curves and spin weights). Raster tiles share the tile disk cache, are decoded once and cached process-wide (released byVectorTileLayer.clearMemoryCache()), overzoom past the source maximum, respect the sourcetileSize(512/256) underTileOffset, and render in-memory ancestors as provisional imagery during zoom — the same lifecycle vector tiles get.StyleReaderpicks raster sources up automatically (inlinetilesor TileJSONurl); passstyle.rasterSourcesto the newVectorTileLayer.rasterSourcesparameter.
1.1.0 #
Reopening a map is now nearly instant. Both caches used to die with the layer, so every time a map screen was pushed it started from nothing.
- ⚡ The disk cache is awaited rather than raced. It initializes asynchronously — its directory comes from a platform channel — but tiles are requested on the very first frame, and the load path read it as a plain field that was still null. Every map open therefore fetched its opening screenful over the network, with the tiles already on disk. The stores now hold the pending cache and await it.
- ⚡ Decoded tiles are cached process-wide instead of per layer, so a
reopened map paints without decoding anything again. Entries are keyed by
source and by the properties the theme reads — two styles over one
source do not share trimmed tiles — and are capped by
VectorTileLayer.memoryCacheMaxBytesper key. They outlive the layer by design;VectorTileLayer.clearMemoryCache()releases them, e.g. from a memory-pressure handler. - ⚡
DiskCacheinstances are shared per directory, so a second layer over the same folder reuses an initialized cache instead of setting up its own. - 📝
VectorTileProvider.cacheKeynow also identifies the in-memory cache. Providers serving different bytes must not share one.
1.0.0 #
First stable release: the public API is now considered settled and will follow semantic versioning from here.
- ✨
icon-color,icon-halo-colorandicon-halo-widthare supported for SDF sprite sheets (see the rendering fix below). - 🐛 The map no longer shakes while zooming and no longer slides out from under its labels while dragging. Tile rectangles are now made camera-relative in Dart before they reach the canvas: world pixel coordinates pass 2^24 around zoom 16, and the canvas transform is float32, so absolute coordinates were snapping the imagery onto a grid — 0.25 px at zoom 14, but 4 px at zoom 18 and 16 px at zoom 20. The label pass does its own float64 arithmetic, so the two drifted apart. This is why the artifacts only appeared when heavily zoomed in.
- 🐛 SDF sprites are rendered correctly instead of as dark blobs.
Sheets flagged
"sdf": truestore a distance field in the alpha channel over flat RGB; they were being blitted directly, which painted the raw field. They are now thresholded and tinted, adding support foricon-color,icon-halo-colorandicon-halo-width. Ordinary sprites are unaffected. Dark MapLibre styles are typically entirely SDF, which is why icons looked correct in light styles and wrong in dark ones. - 🐛
text-opacityis now applied. Labels the style fades out are also no longer laid out at all, so they stop reserving collision space and suppressing the visible labels around them.
0.4.1 #
Nothing on screen disappears before its replacement is ready — the blanking, flashing and dropped frames around zoom changes are gone.
- 🐛 The map no longer blanks and reloads moments after first paint: the async disk-cache initialization now attaches to the running tile stores instead of rebuilding them (which disposed every rendered tile).
- 🐛 No more flash on zoom level changes: the previous level's imagery is kept until the new tiles are rasterized and fully faded in — it used to be dropped mid-fade, dipping to the background color.
- 🐛 Labels no longer blink out on zoom level changes: the previous level's labels keep drawing wherever the new level has no label data yet (current-level labels win collisions as they arrive).
- ⚡ Smoother zooming: tiles are rasterized through a priority queue with a per-frame time budget (viewport centre first) instead of all in one frame. With warm caches, a zoom change used to rasterize the entire grid synchronously in a single frame, dropping frames.
0.4.0 #
Places you have already visited keep working without a network.
- ✈️ Offline support for recently visited places:
StyleReadernow caches the style bundle (style.json, TileJSON, sprites) on disk with stale-while-revalidate: the cached copy is served instantly — including fully offline — and refreshed in the background once older thanrefreshAfter(12 h default). Opt out withcache: false.- Tiles: when a network fetch fails, an expired disk-cache entry is
served instead of a blank tile.
diskCacheTtlis now a freshness window — stale tiles are kept (up to the size cap, evicted oldest first) as the offline fallback instead of being deleted by age. - The default cache folder moved from the system temp directory to the application support directory, which the OS doesn't purge. Existing temp-dir caches are simply abandoned and refetched once.
0.3.0 #
Road labels now follow the curve of their line.
- ✨ Curved line text: road labels now follow their line glyph by glyph,
with MapLibre semantics —
text-max-angle(labels on too-sharp bends are not placed),text-keep-upright(reading direction flips so text is never upside-down) andtext-rotation-alignment: viewport(horizontal shield text). Glyph spacing/kerning comes from the full string layout; per-glyph collision boxes; nearly straight labels are drawn as a single rotated string for speed. - Scripts with contextual shaping (Arabic, Indic, Thai, …) fall back to straight placement so glyphs are never mis-joined; labels longer than their line are dropped instead of sticking out (matches MapLibre).
- Along-line labels no longer wrap to multiple lines.
- ⬆️
latlong2constraint widened to>=0.9.1 <0.11.0, so 0.10.x resolves. Still compatible with flutter_map 8.2.0+, which pins^0.9.1until 8.3.1. - 🧹 Source reformatted with the Dart formatter (no behaviour change).
0.2.0 #
Denser labels and pattern fills, plus a parser fix that quietly broke font stacks and label offsets in most real styles.
- ✨
text-variable-anchor+text-radial-offset: labels try alternate anchors before being dropped on collision — dense areas keep far more POI and place labels visible (matches MapLibre behaviour). - ✨
fill-pattern(andfill-extrusion-pattern): polygon fills render repeating sprite patterns (wetlands, glaciers, pedestrian zones), world-grid aligned across tile seams; missing sprites fall back to the color fill. - 🐛 Bare literal arrays in style JSON (
text-offset: [0, 0.6],text-font: [...],line-dasharray: [2, 1], anchor lists) were mis-parsed as expressions and silently fell back to defaults — affected font stacks and label offsets in most real styles. LabelPainter.paintnow returns the symbols that were actually drawn.
0.1.0 #
Initial release. 🎉
VectorTileLayerfor flutter_map ≥ 8 rendering MapLibre/Mapbox GL styles from MVT vector tile sources.- Self-contained: built-in MVT decoder, style/expression engine (modern expressions + legacy filters/functions/tokens), sprite support.
- GPU-resident per-tile rasterization (
Picture.toImageSync) with fade-in, ancestor-tile retention and provisional rendering from cached parent tiles (no white flashes on zoom). - Screen-space label pass with global cross-tile collision, upright text under rotation, halo and sprite icon support.
- Worker-isolate decode pipeline with priority ordering and silent cancellation.
- Deterministic LRU memory caches (byte budgets) and TTL + size-capped disk cache.
StyleReaderfor style.json / TileJSON / sprites with{key}substitution, tolerant of real-world provider quirks (MapTiler, OpenFreeMap, Stadia, ArcGIS, Protomaps).TileOffset.maplibredefault for correct 512px-convention rendering.