Skip to content

Latest commit

Β 

History

73 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
  • πŸ›£οΈ 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 LatLngBounds during 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

πŸ“¦ 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,
)

Camera bounds and fit bounds

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.

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.

Cluster markers

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.

Polylines (routes)

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.

πŸ—οΈ 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
    β”‚   β”œβ”€β”€ 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

🎯 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