Skip to content

Latest commit

Β 

History

46 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

FOSM - Flutter OpenStreetMap

A high-performance Flutter map library with native raster and vector tile rendering, featuring OpenFreeMap integration, persistent background isolates, and advanced performance optimizations.

Flutter Version Dart Version License

✨ Features

  • πŸ—ΊοΈ 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
  • πŸš€ 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)
  • 🧡 Background Processing: Compute-based protobuf parsing on separate threads
  • πŸ“Š Pre-loading: Intelligent adjacent zoom level pre-loading

πŸ“¦ Installation

dependencies:
  fosm:
    git:
      url: https://github.com/yourusername/fosm.git

πŸš€ Quick Start

Initialize Map Cache

import 'package:fosm/fosm.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initMap(); // Initialize Hive cache
  runApp(const MyApp());
}

Raster Map (OpenStreetMap)

MapView(
  latLng: LatLng(latitude: 47.4358, longitude: 8.4737),
  zoom: 10,
  minZoom: 1,
  maxZoom: 19,
  showZoomControls: true,
  animateZoom: true,
)

Vector Map (OpenFreeMap Liberty)

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');
  },
)

Custom Vector Style

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,
)

Markers

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.

Marker gestures & overlays

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.

πŸ—οΈ Architecture

High-Level Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         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        β”‚      β”‚                          β”‚      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

🧡 Isolate Architecture

FOSM uses a sophisticated isolate strategy to keep the UI responsive while performing heavy operations.

HTTP Isolate (Persistent)

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)

Compute Isolates (Per-Call)

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()

Web Platform

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 responsive

🎨 Vector Tile Rendering

Rendering Pipeline

MVT 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)

Chunked Rendering

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 & Icons

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

πŸ”„ Caching Strategy

Memory Cache (LRU)

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)

Byte Cache (Pre-loaded 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

Disk Cache (Hive)

await storeTile(key, tile, bytes);           // Write
final bytes = await storedTileBytes(key);    // Read

Features:

  • Persistent across app restarts
  • Separate namespace per vector style
  • Automatic corruption detection (delete + re-download)

πŸ“Š Performance Optimizations

1. Center-First Tile Loading

// 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.

2. Raster Layer Skip

// 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.

3. Dash Line Optimization

// 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.

4. Concurrent Decode Limiting

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.

5. Pre-loading Strategy

// 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.

πŸ“ Project Structure

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
    β”‚   └── marker_manager.dart        # Marker collection (ChangeNotifier)
    β”‚
    β”œβ”€β”€ view/
    β”‚   β”œβ”€β”€ map_view.dart              # Main map widget
    β”‚   β”œβ”€β”€ render.dart                # CustomPainters (tiles, labels)
    β”‚   β”œβ”€β”€ 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

🎯 Supported Mapbox Style Features

Layer Types

  • βœ… 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

Expressions

  • βœ… 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

Paint Properties

Fill:

  • fill-color, fill-opacity, fill-outline-color

Line:

  • line-color, line-opacity, line-width
  • line-dasharray, line-cap, line-join

Circle:

  • circle-color, circle-opacity, circle-radius
  • circle-stroke-color, circle-stroke-width

Symbol:

  • text-color, text-halo-color, text-halo-width
  • text-size, text-font, text-letter-spacing
  • icon-image, icon-size

🌐 OpenFreeMap

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 vectorStyle

πŸ”§ Advanced Usage

Custom Tile Source

MapView(
  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);
  },
)

Custom Vector Style

const myStyle = VectorMapStyle(
  id: 'my-style',
  styleUrl: 'https://example.com/style.json',
);

MapView(
  vectorStyle: myStyle,
  // ...
)

Disable Animations

MapView(
  animateZoom: false,
  zoomAnimationDuration: Duration(milliseconds: 200),
  // ...
)

Zoom Callback

MapView(
  onZoomChanged: (zoom) {
    print('Current zoom: $zoom');
    setState(() => _currentZoom = zoom);
  },
)

πŸ§ͺ Testing

# Run all tests
flutter test

# Run with coverage
flutter test --coverage

# Run specific test file
flutter test test/vector/vector_tile_test.dart

Test Coverage: 86 tests passing

  • Vector tile parsing
  • Style expression evaluation
  • Tile manager logic
  • Grid calculations
  • Cache behavior

πŸ“ˆ Performance Benchmarks

Tile Loading (Native - macOS)

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

Tile Loading (Web - Chrome)

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

Impact of Optimizations

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

🀝 Contributing

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

πŸ“„ License

MIT License - see LICENSE for details.

πŸ™ Acknowledgments

πŸ“š Related Projects


Made with ❀️ by the FOSM Team

High-performance maps for Flutter, without the complexity.

About

native Flutter osm map

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages