A high-performance Flutter map library with native raster and vector tile rendering, featuring OpenFreeMap integration, persistent background isolates, and advanced performance optimizations.
- πΊοΈ Dual Rendering Modes: Native raster (OSM) and vector (Mapbox Vector Tiles) rendering
- π Markers: Any Flutter widget (or plain text) anchored to lat/lon via
MarkerManagerβ with tap/long-press gestures (hand cursor on hover) and marker-following overlays - π£οΈ Polylines: Render decoded route geometry (
List<LatLng>) in any color/width, above tiles and below markers β works in raster and vector mode - π High Performance: Persistent HTTP isolate with TCP connection reuse
- π― Smart Caching: Memory + disk (Hive) with intelligent eviction
- π OpenFreeMap Integration: Free, no API key required vector tiles
- π± Cross-Platform: iOS, Android, Web, macOS, Linux, Windows
- π·οΈ Labels & Icons: Point labels, line labels (road names), and sprite icons
- π Zoom Animations: Google Maps-style two-phase transitions β blurred hold while tiles load, then scale reveal. Two intensity levels (
scale/crossfade) - Camera Bounds: Keep the camera inside a fixed
LatLngBoundsduring gestures and programmatic movement - Fit Bounds: Frame routes and other geographic boxes with optional padding through
MapController.fitBounds - π§΅ Background Processing: Compute-based protobuf parsing on separate threads
- π Pre-loading: Intelligent adjacent zoom level pre-loading
dependencies:
fosm:
git:
url: https://github.com/yourusername/fosm.gitimport 'package:fosm/fosm.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initMap(); // Initialize Hive cache
runApp(const MyApp());
}MapView(
latLng: LatLng(latitude: 47.4358, longitude: 8.4737),
zoom: 10,
minZoom: 1,
maxZoom: 19,
showZoomControls: true,
animateZoom: true,
)MapView(
latLng: LatLng(latitude: 47.4358, longitude: 8.4737),
zoom: 10,
minZoom: 1,
maxZoom: 19,
vectorStyle: openFreeMapLiberty, // Built-in preset
showZoomControls: true,
animateZoom: true,
onZoomChanged: (zoom) {
print('Zoom level: $zoom');
},
)const customStyle = VectorMapStyle(
id: 'my-custom-style',
styleUrl: 'https://tiles.openfreemap.org/styles/bright',
);
MapView(
latLng: LatLng(latitude: 47.4358, longitude: 8.4737),
zoom: 10,
vectorStyle: customStyle,
)Use cameraBounds when the camera must remain inside a fixed geographic box.
The constraint applies to gestures, zoom focal-point changes, and programmatic
camera movement:
final controller = MapController();
final allowedArea = LatLngBounds(
southwest: LatLng(latitude: 47.20, longitude: 8.20),
northeast: LatLng(latitude: 47.70, longitude: 8.80),
);
MapView(
controller: controller,
latLng: allowedArea.center,
zoom: 10,
cameraBounds: allowedArea,
);Use fitBounds to navigate to a box without creating a persistent constraint.
This is useful for framing routes after the map controller is attached:
final routeBounds = LatLngBounds.fromPoints(routePoints);
controller.fitBounds(
routeBounds,
padding: const EdgeInsets.all(48),
animate: true,
);LatLngBounds accepts finite coordinates in the Web Mercator latitude range
and longitudes from -180 to 180. Bounds crossing the antimeridian are not
supported. Remove a persistent constraint by rebuilding MapView with
cameraBounds: null.
Own a MarkerManager, pass it to the map, and mutate it at runtime β the
map re-renders on every change:
final markers = MarkerManager();
MapView(
latLng: const LatLng(latitude: 47.4358, longitude: 8.4737),
zoom: 10,
markers: markers,
);
// Any widget, anchored so its bottom-center tip sits on the coordinate:
markers.add(
const Marker(
point: LatLng(latitude: 47.4358, longitude: 8.4737),
alignment: Alignment.bottomCenter,
child: Icon(Icons.location_on, size: 40, color: Colors.red),
),
);
// Or plain text:
markers.add(Marker.text('Zurich', const LatLng(latitude: 47.3769, longitude: 8.5417)));
// Remove individually (by identity), filter, or clear:
markers.remove(firstMarker);
markers.removeWhere((m) => /* β¦ */);
markers.clear();Markers render above the tile grid (below vector labels) and track the
camera on every pan and zoom. Markers whose anchor leaves the viewport
are culled β they are not built, laid out, or painted until visible
again. alignment picks which point of the widget sits on the
coordinate (default: center; use Alignment.bottomCenter for pins).
Marker children are ordinary widgets, so buttons and gesture handlers
work inside them.
Markers accept onTap / onLongPress callbacks, and any marker with an
overlayBuilder gets a tap-to-toggle overlay ("info window") β any
Flutter widget, anchored to the marker and following it across pans and
zooms:
markers.add(
Marker(
point: const LatLng(latitude: 47.4358, longitude: 8.4737),
alignment: Alignment.bottomCenter,
onTap: () => print('tapped'), // fires alongside the toggle
onLongPress: () => print('long press'),
overlayBuilder: (context) => const Card(
child: Padding(
padding: EdgeInsets.all(8),
child: Text('Hello from Zurich'),
),
),
child: const Icon(Icons.location_on, size: 40, color: Colors.red),
),
);Behavior is shaped per-marker with MarkerOverlayConfig:
| Option | Default | Behavior |
|---|---|---|
removeOnMove |
false |
true dismisses the overlay as soon as the camera changes (pan, zoom, or programmatic); false keeps it anchored while it follows the marker |
closeOnMapTap |
true |
dismisses the overlay when the user taps the bare map or a marker without its own overlay; taps inside the overlay never dismiss it |
anchor |
above |
side of the marker the overlay sits on: above, below or center |
offset |
Offset(0, 8) |
extra gap between marker and overlay |
animationDuration |
200 ms |
fade + scale entrance; Duration.zero shows instantly |
One overlay is visible at a time. The manager exposes programmatic
control and listeners (which also fire for automatic dismissals like
removeOnMove and map taps):
markers.showOverlay(myMarker); // returns false if marker has no overlayBuilder
markers.hideOverlay();
markers.overlayMarker; // whose overlay is open, or null
markers.onOverlayShown = (marker) { /* β¦ */ };
markers.onOverlayHidden = (marker) { /* β¦ */ };Removing a marker (or clearing the manager) while its overlay is open hides the overlay automatically.
For large marker sets, replace some or all Markers with ClusterMarkers and
tell the map how to group them:
final markers = MarkerManager();
markers.addAll([
ClusterMarker(
point: LatLng(latitude: 47.3769, longitude: 8.5417),
child: const Icon(Icons.circle, size: 16, color: Colors.green),
),
ClusterMarker(
point: LatLng(latitude: 47.3775, longitude: 8.5420),
child: const Icon(Icons.circle, size: 16, color: Colors.green),
),
]);
MapView(
latLng: const LatLng(latitude: 47.3769, longitude: 8.5417),
zoom: 10,
markers: markers,
markerClusterOptions: const MarkerClusterOptions(
radius: 64, // group members within this many screen pixels
maxZoom: 14, // disable clustering above this zoom level
minSize: 2, // smallest group that forms a cluster
zoomOnTap: true, // zoom in one level when a cluster is tapped
),
)Use clusterGroup to cluster markers independently per category (e.g. stores
vs restaurants). Provide a builder to customize the cluster badge, or leave it
out to use the default circular count badge:
MarkerClusterOptions(
radius: 64,
builder: (context, cluster) => Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Colors.deepPurple,
shape: BoxShape.circle,
),
child: Text('${cluster.count}', style: const TextStyle(color: Colors.white)),
),
onTap: (cluster) => print('Tapped cluster of ${cluster.count}'),
)Plain Markers and ClusterMarkers can coexist in the same manager. Only
ClusterMarkers are grouped; plain markers are always rendered individually
and render above generated clusters. Tapping a cluster dispatches
MapMarkerClusterTapNotification and fires MarkerClusterOptions.onTap, so
MapEventListenerMixin.onMapMarkerClusterTapped works too.
FOSM renders decoded route geometry only β calling a routing provider
(OSRM, OpenRouteService, β¦) and decoding its encoded-polyline response is
your application's responsibility. Pass the decoded LatLng points as
MapPolylines:
final routePoints = <LatLng>[
const LatLng(latitude: 47.3769, longitude: 8.5417),
const LatLng(latitude: 47.3788, longitude: 8.5470),
const LatLng(latitude: 47.3811, longitude: 8.5524),
];
MapView(
latLng: routePoints.first,
zoom: 14,
polylines: [
MapPolyline(
points: routePoints,
color: Colors.blue,
strokeWidth: 5, // logical pixels; does not scale with zoom
),
],
);Replacing or clearing a route is ordinary widget configuration β rebuild the parent with a new list (empty lists and one-point lists paint nothing):
polylines: routePoints.isEmpty
? const []
: [MapPolyline(points: routePoints)],Multiple independently styled routes are supported; list order is paint order (later polylines draw above earlier ones):
polylines: [
MapPolyline(points: fastestRoute, color: Colors.blue),
MapPolyline(points: alternativeRoute, color: Colors.grey),
],Routes support solid, dashed, and dotted patterns, optional outer borders/casing, and configurable caps, joins, and miter limits:
// Solid route with a white border.
MapPolyline(
points: routePoints,
color: const Color(0xFF3F51B5),
strokeWidth: 6,
borderColor: Colors.white,
borderWidth: 2,
)
// Dashed alternative route.
MapPolyline(
points: alternativeRoute,
color: Colors.orange,
strokeWidth: 5,
pattern: const MapPolylinePattern.dashed(
dashLength: 14,
gapLength: 8,
),
strokeCap: StrokeCap.round,
strokeJoin: StrokeJoin.round,
)
// Dotted walking route.
MapPolyline(
points: walkingRoute,
color: Colors.deepPurple,
strokeWidth: 6, // dot diameter
borderColor: Colors.white,
borderWidth: 1,
pattern: const MapPolylinePattern.dotted(spacing: 12),
)Pattern dimensions, stroke widths, and border widths are in logical pixels
and do not scale with map zoom. Dash-and-gap cycles and dot spacing must be
at least one logical pixel. Routes render above the tile grid (and the
zoom-transition overlay) and below markers and vector labels, and stay
geographically aligned through pan, pinch zoom, zoom controls, and
MapController movements. Route painting never consumes gestures.
Known limitation: a segment crossing the international date line
(longitude 179 β -179) is drawn as a long straight line across the map
rather than wrapping around the world.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User Interface β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β MapView Widget β β
β β ββββββββββββββ ββββββββββββββ ββββββββββββββββββ β β
β β β Gesture β β Zoom β β Label β β β
β β β Handler β β Controls β β Overlay β β β
β β ββββββββββββββ ββββββββββββββ ββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Tile Manager β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ β
β β Grid Calc β β Memory Cache β β Pre-loading β β
β β (viewport) β β (LRU, 200) β β Manager β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Rendering Pipeline β
β ββββββββββββββββββ ββββββββββββββββββββββββββ β
β β Raster Mode β β Vector Mode β β
β β β β β β
β β Image Decode β β MVT Parse β Style Eval β β
β β (PNG/JPEG) β β β Canvas Render β β
β ββββββββββββββββββ ββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Isolate Architecture β
β ββββββββββββββββββββ ββββββββββββββββββββββββββ β
β β HTTP Isolate β β Compute Isolates β β
β β (Persistent) β β (Per-Call) β β
β β β β β β
β β β’ TCP Reuse β β β’ MVT Parsing β β
β β β’ Connection β β β’ CPU-intensive work β β
β β Pooling β β β β
β ββββββββββββββββββββ ββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
FOSM uses a sophisticated isolate strategy to keep the UI responsive while performing heavy operations.
A long-lived background isolate that handles ALL network I/O:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main Thread β
β β
β TileManager β
β ββ Visible tile request ββββββββββββββ β
β ββ Preload request βββββββββββββββββββ β
β ββ β
βββββββββββββββββββββββββββββββββββββββββΌβΌββββββββββββββββββββββ
ββ
ββββββββββββββββββββββ
β SendPort(url) β
βΌ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HTTP Isolate (Background) β
β β
β Persistent HttpClient β
β ββ Connection timeout: 10s β
β ββ Idle timeout: 30s β
β ββ TCP Connection Pool ββββββββββββββββββββββββββββββ β
β β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β’ Reuses TCP connections across hundreds of ββ β
β β tile requests ββ β
β β β’ Avoids TLS handshake overhead ββ β
β β β’ Reduces latency by ~50-100ms per request ββ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β Response: Uint8List βββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β SendPort(bytes)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main Thread β
β β
β Decode & Render β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Benefits:
- β TCP connection reuse (no repeated TLS handshakes)
- β Reduced latency (~50-100ms saved per request)
- β Network I/O completely off main thread
- β Single isolate for all tile types (raster + vector)
Short-lived isolates for CPU-intensive work:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main Thread β
β β
β VectorTileRuntime β
β ββ decodeMvtAsync(bytes) ββββββββββββββββββββββββββββββ β
β β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΌβββββ
β
compute() spawns β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Compute Isolate (Temporary) β
β β
β decodeVectorTile(bytes) β
β ββ ProtobufReader (parse MVT) β
β ββ Extract layers, features, properties β
β ββ Decode geometry (coordinates) β
β ββ Build DecodedVectorTile βββββββββββββββββββββββββ β
β β β
β Duration: ~2-5ms β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββΌββββββββ
β
Return value β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main Thread β
β β
β DecodedVectorTile β
β ββ Render on Canvas (fills, lines, labels, icons) β
β Duration: ~15-25ms β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why not persistent parse isolate?
- Compute isolates use
Isolate.exit()which returns values efficiently - Spawn overhead (~1-3ms) is negligible vs parsing time (2-5ms)
- Persistent isolates have complex serialization issues with Dart objects
- Tests work reliably with compute()
On web, isolates work differently:
// HTTP Isolate: Disabled (browsers handle connection pooling)
if (kIsWeb) {
// Use Dio on main thread
bytes = await downloadTileBytes(url);
} else {
// Use persistent isolate on native
bytes = await _httpIsolate.fetchUrl(url);
}
// MVT Parsing: compute() runs on main thread (no isolates on web)
// But we yield between operations to keep UI responsiveMVT Bytes (.pbf)
β
βΌ
βββββββββββββββββββ
β Protobuf Parse β compute() isolate (native)
β β or main thread (web)
βββββββββββββββββββ
β
βΌ
DecodedVectorTile
β
ββ Layers (water, roads, buildings, etc.)
ββ Features with properties
ββ Geometry (coordinates)
β
βΌ
βββββββββββββββββββ
β Style Eval β Apply Mapbox Style Spec
β β - Expressions (interpolate, match, etc.)
β β - Filters
β β - Paint properties
βββββββββββββββββββ
β
βΌ
βββββββββββββββββββ
β Canvas Render β Chunked rendering (8 layers/batch)
β β - Fills (polygons)
β β - Lines (roads, rivers)
β β - Circles (POIs)
β β - Yield every batch (web)
βββββββββββββββββββ
β
βΌ
ui.Picture
β
βΌ
βββββββββββββββββββ
β toImage(256x256)β GPU β CPU readback
βββββββββββββββββββ
β
βΌ
ui.Image (cached)
To prevent UI freezes on web, rendering is split into batches:
Future<ui.Picture> renderAsync({
required DecodedVectorTile decoded,
required int z, int x, int y,
}) async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
var layerCount = 0;
for (final layer in visibleLayers) {
_paintLayer(canvas, layer, decoded);
layerCount++;
// Yield every 8 layers on web
if (kIsWeb && layerCount % 8 == 0) {
await Future<void>.delayed(Duration.zero);
}
}
return recorder.endRecording();
}Performance Impact:
BEFORE: [βββββββββββββββββββββββββββββββββββββββ] 25ms freeze
AFTER: [βββ][yield][βββ][yield][βββ][yield][βββ] ~3ms chunks
Labels are rendered as an overlay (not baked into tiles):
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Viewport Canvas β
β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Tile Image β β Tile Image β β Tile Image β β
β β (256x256) β β (256x256) β β (256x256) β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Label Overlay β β
β β β β
β β β’ Point Labels (cities, POIs) β β
β β β’ Line Labels (road names, rivers) β β
β β β’ Icons (airports, stations, etc.) β β
β β β β
β β Collision Detection: β β
β β ββββββββββ β β
β β β Label ββββ Checks overlap with placed labels β β
β β ββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Line Labels:
- Sampled every 250px along paths
- Rotated to follow road/river direction
- Flipped if upside-down (keeps text readable)
Collision Detection:
- Uniform grid (72px cells)
- First-come-first-served placement
- Prevents label overlap
final LinkedHashMap<String, ui.Image> _memoryCache = LinkedHashMap();
static const int maxMemoryCachedTiles = 200;
void _trimMemoryCache() {
while (_memoryCache.length > maxMemoryCachedTiles) {
final oldestKey = _memoryCache.keys.first;
final image = _memoryCache.remove(oldestKey);
if (!isVisible(oldestKey)) {
image.dispose(); // Free GPU memory
}
}
}Benefits:
- O(1) access time
- Automatic eviction of least-recently-used tiles
- GPU memory management (dispose off-screen tiles)
final Map<String, Uint8List> _byteCache = {};
static const int maxByteCacheBytes = 50 * 1024 * 1024; // 50MB
void _storeInByteCache(String key, Uint8List bytes) {
_byteCache[key] = bytes;
_trimByteCache();
}Purpose:
- Store pre-loaded adjacent zoom tiles (Β±1 levels)
- Instant decode when zooming (no network wait)
- Compressed bytes (PNG/MVT) use less memory than decoded images
await storeTile(key, tile, bytes); // Write
final bytes = await storedTileBytes(key); // ReadFeatures:
- Persistent across app restarts
- Separate namespace per vector style
- Automatic corruption detection (delete + re-download)
// Sort tiles by distance from viewport center
final tiles = visibleTiles
..sort((a, b) => a.distanceToCenter.compareTo(b.distanceToCenter));
for (final tile in tiles) {
_scheduleLoad(tile);
}Result: Center tiles render first, edges fill in progressively.
// Only fetch raster tiles if visible at current zoom
final hasVisibleRaster = layers.any((l) =>
l.type == LayerType.raster &&
l.isVisible &&
zoom >= l.minZoom &&
zoom <= l.maxZoom);
if (hasVisibleRaster) {
// Fetch raster tiles
}Impact: OpenFreeMap Liberty's relief layer only visible at zoom < 5. Skipped at zoom 5+, saving network requests.
// Skip dash computation for hairline widths
if (hasDash && width >= 0.5) {
_drawDashed(canvas, path, paint);
} else {
canvas.drawPath(path, paint);
}Why: Dash patterns imperceptible below 0.5px width.
static const int maxConcurrentDecodes = 3;
Future<ui.Image> decoder(bytes, z, x, y) async {
await _waitForDecodeSlot();
try {
return await _decodeAndRender(bytes, z, x, y);
} finally {
_releaseDecodeSlot();
}
}Purpose: Prevent overwhelming the GPU with simultaneous toImage() calls.
// Pre-load Β±1 zoom levels after 500ms idle
if (idleTime > 500ms) {
_preloadAdjacentZoom();
}
// Max 20 concurrent preloads
static const int maxConcurrentPreloads = 20;Result: Smooth zoom transitions with instant tile availability.
lib/
βββ fosm.dart # Public API exports
βββ src/
βββ api/
β βββ tile.dart # Tile data class
β βββ tile_manager.dart # Grid calculation & loading
β βββ tile_source.dart # TileFetcher typedef
β βββ geo_point.dart # LatLng class
β βββ marker.dart # Marker model
β βββ map_polyline.dart # MapPolyline model (route styling)
β βββ marker_manager.dart # Marker collection (ChangeNotifier)
β
βββ view/
β βββ map_view.dart # Main map widget
β βββ render.dart # CustomPainters (tiles, labels)
β βββ polyline_layer.dart # Route/polyline rendering layer
β βββ marker_layer.dart # Widget markers + viewport culling
β βββ zoom_controls.dart # +/- buttons
β
βββ vector/
β βββ mvt/
β β βββ protobuf_reader.dart # Protobuf decoder
β β βββ vector_tile.dart # MVT parser
β β
β βββ render/
β β βββ vector_tile_runtime.dart # Tile lifecycle
β β βββ vector_tile_renderer.dart # Canvas rendering
β β βββ label_overlay.dart # Labels & icons
β β βββ sprite_atlas.dart # Icon sprites
β β
β βββ style/
β βββ style_loader.dart # Load Mapbox style JSON
β βββ expression.dart # Style expressions
β βββ css_color.dart # Color parsing
β
βββ isolate/
β βββ http_isolate.dart # HTTP isolate (stub)
β βββ http_isolate_native.dart # HTTP isolate (native)
β βββ mvt_worker.dart # MVT parsing (compute)
β
βββ common/
βββ utils.dart # Helpers
βββ osm_transformation_utilities.dart # Math
βββ cache_tile_mixin.dart # Hive cache
test/
βββ vector/
β βββ vector_tile_test.dart
β βββ style_parser_test.dart
β βββ expression_test.dart
βββ marker_manager_test.dart
βββ marker_layer_test.dart
βββ tile_manager_test.dart
- β
background- Solid color background - β
fill- Polygon fills - β
line- Lines (roads, rivers, borders) - β
circle- Circles (POIs) - β
symbol- Text labels and icons - β
raster- Raster imagery - β³
fill-extrusion- 3D buildings (planned) - β
heatmap- Not supported - β
hillshade- Not supported
- β
Arithmetic:
+,-,*,/ - β
Comparison:
==,!=,<,<=,>,>= - β
Logical:
all,any,none - β
Interpolation:
interpolate(linear, exponential) - β
Matching:
match,step,case,coalesce - β
Property access:
get,has - β
Type conversion:
to-number,to-string,to-color - β
String:
concat
Fill:
fill-color,fill-opacity,fill-outline-color
Line:
line-color,line-opacity,line-widthline-dasharray,line-cap,line-join
Circle:
circle-color,circle-opacity,circle-radiuscircle-stroke-color,circle-stroke-width
Symbol:
text-color,text-halo-color,text-halo-widthtext-size,text-font,text-letter-spacingicon-image,icon-size
FOSM includes built-in support for OpenFreeMap:
// Built-in preset
MapView(
vectorStyle: openFreeMapLiberty,
// ...
)Features:
- β Free, no API key required
- β Vector tiles (Mapbox Vector Tiles format)
- β Multiple styles: Liberty, Bright, Dark, Positron
- β Global coverage
- β High-performance CDN
Attribution Required:
// Automatically displayed when using vectorStyleMapView(
latLng: LatLng(latitude: 0, longitude: 0),
zoom: 2,
tileFetcher: (z, x, y) async {
final url = 'https://my-tile-server.com/$z/$x/$y.png';
return await downloadTileBytes(url);
},
)const myStyle = VectorMapStyle(
id: 'my-style',
styleUrl: 'https://example.com/style.json',
);
MapView(
vectorStyle: myStyle,
// ...
)MapView(
animateZoom: false,
zoomAnimationDuration: Duration(milliseconds: 200),
// ...
)MapView(
onZoomChanged: (zoom) {
print('Current zoom: $zoom');
setState(() => _currentZoom = zoom);
},
)# Run all tests
flutter test
# Run with coverage
flutter test --coverage
# Run specific test file
flutter test test/vector/vector_tile_test.dartTest Coverage: 86 tests passing
- Vector tile parsing
- Style expression evaluation
- Tile manager logic
- Grid calculations
- Cache behavior
| Operation | Time |
|---|---|
| HTTP fetch (cached connection) | ~50ms |
| MVT protobuf parse | ~2-5ms |
| Style evaluation + render | ~15-25ms |
toImage(256x256) |
~5-15ms |
| Total per tile | ~70-95ms |
| Operation | Time |
|---|---|
| HTTP fetch (browser) | ~100ms |
| MVT protobuf parse | ~2-5ms |
| Style evaluation + render | ~15-25ms |
toImage(256x256) |
~5-15ms |
| Total per tile | ~120-145ms |
| Optimization | Latency Saved |
|---|---|
| Persistent HTTP isolate (TCP reuse) | 50-100ms |
| Chunked rendering (web) | Prevents 25ms freezes |
| Center-first loading | Perceived +500ms |
| Pre-loading Β±1 zoom | Instant zoom transitions |
Contributions welcome! Areas of focus:
- 3D building extrusion (flutter_gpu)
- Line label collision detection
- Terrain/3D globe projection
- Offline map packages
- Custom layer rendering
- Performance profiling tools
MIT License - see LICENSE for details.
- OpenFreeMap - Free vector tiles
- OpenStreetMap - Map data
- Mapbox - Vector tile specification
- Hive - Fast key-value database
- flutter_map - Alternative Flutter map library
- maplibre_gl - MapLibre GL bindings
Made with β€οΈ by the FOSM Team
High-performance maps for Flutter, without the complexity.