powermap_sdk 0.10.0
powermap_sdk: ^0.10.0 copied to clipboard
A comprehensive Flutter SDK for PowerMap, offering location plotting, geometry measurements, layer management, routing, search, and tracking.
PowerMap Flutter SDK π #
A professional-grade, high-performance Map SDK for Flutter, built with a Powerful Native Engine. Optimized for logistics, real-time tracking, indoor mapping, and buttery-smooth 60FPS mobile map interactions with deep architectural parity to the PowerMap Web SDK.
1. π¦ Installation #
Add the package to your pubspec.yaml:
dependencies:
powermap_sdk: ^0.8.0
Then, run:
flutter pub get
2. ποΈ Architecture Overview #
The PowerMap Flutter SDK is built with a Layered Facade Architecture, ensuring that high-performance native rendering is wrapped in an easy-to-use Flutter API.
graph TD
App[Flutter App] --> PowerMap[PowerMap Widget]
PowerMap --> Controller[PowerMapController]
subgraph "Facade Layer"
Controller
end
subgraph "Manager Layer"
Controller --> MarkerMgr[MarkerManager]
Controller --> SearchMgr[SearchManager]
Controller --> RouteMgr[RoutingManager]
Controller --> GeomMgr[GeometryManager]
Controller --> TrackingMgr[TrackingManager]
Controller --> LayerMgr[LayerManager]
Controller --> IndoorMgr[IndoorManager]
end
subgraph "Service Layer"
MarkerMgr --> TileSvc[TileService]
SearchMgr --> GeoSvc[GeocodingProvider]
TileSvc --> Cache[3-Layer Cache]
end
subgraph "Native Engine"
PowerMap --> NativeEngine[High-Performance Native Rendering]
end
Cache -.-> Disk[(Local Storage)]
Cache -.-> CDN((PowerMap CDN))
- Unified Facade:
PowerMapControllerprovides a single entry point for all features, matching the Web SDK's DX. - Native Rendering: Symbols, Lines, and Polygons are rendered in C++/OpenGL via our custom native engine for 60FPS performance.
- 3-Layer Caching: Automated Memory/Disk/Network caching ensures offline resilience and fast style loading.
- Hybrid Overlay: Arbitrary Flutter Widgets can be used as markers, perfectly synced with the map camera.
3. π Quick Start #
Get a map up and running in less than 2 minutes. The SDK provides a powerful PowerMapController Facade.
Step A: Initialize Dual-Auth #
Setup the SDK with your API keys globally in your main.dart.
import 'package:flutter/material.dart';
import 'package:powermap_sdk/powermap_sdk.dart';
void main() {
PowerMapSDK.initialize(
mapApiKey: "YOUR_MAP_API_KEY", // Public key for tiles/base layers
clientId: "YOUR_CLIENT_ID", // Core Service Credentials
clientSecret: "YOUR_CLIENT_SECRET",
);
runApp(const MyApp());
}
Step B: The Map Widget #
class MapPage extends StatefulWidget {
@override
_MapPageState createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> {
PowerMapController? map;
@override
Widget build(BuildContext context) {
return Scaffold(
body: PowerMap(
initialCenter: const LatLng(13.7563, 100.5018), // Bangkok
initialZoom: 14.0,
mapStyle: 'dark', // 'th', 'en', 'dark', 'gray'
onMapCreated: (controller) => setState(() => map = controller),
onMapClick: (coords) => print("Tapped at: $coords"),
),
);
}
}
4. π οΈ Configuration Reference #
PowerMap (Widget Properties) #
| Parameter | Type | Default | Description |
|---|---|---|---|
initialCenter |
LatLng |
LatLng(13.7563, 100.5018) |
The initial center point of the map. |
initialZoom |
double |
14.0 |
Initial viewport zoom level (0-22). |
initialTilt |
double? |
0.0 |
Initial pitch of the camera (0-60). |
mapStyle |
String |
'th' |
Base tile style. Available: 'th', 'en', 'dark', 'gray'. |
enable3DBuildings |
bool |
true |
Allows rendering of 3D fill-extrusion layers when the map is tilted. |
onMapCreated |
Function(PowerMapController) |
null |
Triggered when the GL context is fully initialized. |
onMapClick |
Function(LatLng) |
null |
Coordinate where the user tapped. |
onMapLongClick |
Function(LatLng) |
null |
Coordinate where user long-pressed. |
onPoiTapped |
Function(Map<String, dynamic>) |
null |
Fired when a base map POI or custom Geometry is tapped. Returns GeoJSON feature. |
onError |
Function(PowerMapException) |
null |
Global unified callback to catch map loading, network, or auth errors natively. |
5. π Marker Management (map.markers, map.clusters) #
The SDK renders native Symbol Layers for highest drawing performance. Markers support full customization matching the Web SDK's capabilities.
Core Marker Methods #
| Method | Arguments | Description |
|---|---|---|
addMarker(position, ...) |
LatLng, {color?, iconImage?, size?, data?, opacity?, iconRotate?} |
Adds a GL-rendered map symbol marker. Best for performance. |
addWidgetMarker(position, widget, ...) |
LatLng, widget, {id?, alignment?} |
Overlays a Flutter Widget as a marker. Best for complex UI. |
setMarkersOpacity(opacity) |
double (0.0β1.0) |
Bulk-sets the opacity of all native markers. Use with AnimationController for smooth fade effects. |
clearMarkers() |
- | Removes all GL symbols and circles. |
clearWidgetMarkers() |
- | Removes all Flutter widget markers. |
1. GL Symbol Markers (High Performance) #
Best for large datasets (100+ points). Rendered directly by the GPU.
await map?.addMarker(
LatLng(13.76, 100.50),
color: '#f97316',
iconImage: 'step_icon_straight',
iconRotate: 90.0, // 0-360 degree bearing rotation
);
addMarker() Options
| Option | Type | Default | Description |
|---|---|---|---|
position |
LatLng |
Required | Coordinates for the marker. |
color |
String? |
null |
Hex color code. If set without iconImage, renders a GL Circle pinpoint. |
iconImage |
String? |
marker-o.png |
Asset path or registered image key for the icon. Uses bundled default if omitted. |
size |
double |
1.0 |
Scale factor for the icon or circle radius. |
data |
Map<String, dynamic>? |
null |
Metadata attached to the marker (accessible on tap). |
opacity |
double |
1.0 |
Initial visibility of the marker (0.0 = invisible, 1.0 = fully visible). Use with setMarkersOpacity() to animate a fade-in. |
iconRotate |
double? |
null |
Rotation angle in degrees (0Β°..360Β°) for directional symbols. |
2. Flutter Widget Markers (High Interactivity) #
Best for complex UIs, buttons, or animations inside a marker. These markers are overlaid in a Stack and synchronized with the map's camera using pixel projection.
map?.addWidgetMarker(
LatLng(13.75, 100.48),
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(blurRadius: 4, color: Colors.black26)],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.restaurant, color: Colors.orange, size: 16),
SizedBox(width: 4),
Text("Delicious Cafe", style: TextStyle(fontSize: 12)),
],
),
),
alignment: Alignment.bottomCenter,
);
addWidgetMarker() Options #
| Option | Type | Default | Description |
|---|---|---|---|
position |
LatLng |
Required | Geographic location to pin the widget. |
widget |
Widget |
Required | The Flutter widget to display. |
id |
String? |
null |
Optional unique identifier. |
alignment |
Alignment |
Alignment.center |
Anchor point of the widget relative to the position. |
Bundled Icon Assets #
The SDK ships with 290+ marker icons copied directly from the Web SDK's asset library (assets/markers/). These include category icons, route waypoint markers, and the default pinpoint.
// 1. Default marker (uses bundled marker-o.png automatically)
await map?.addMarker(LatLng(13.7563, 100.5018));
// 2. Custom colored GL pinpoint (no image needed, like Web SDK)
await map?.addMarker(
LatLng(13.76, 100.50),
color: '#f97316', // Vibrant Orange
size: 1.5,
data: {'name': 'My Store'},
);
// 3. Custom icon from bundled assets
await map?.addMarker(
LatLng(13.77, 100.51),
iconImage: 'packages/powermap_sdk/assets/markers/0101.png',
);
// 4. High-Performance Clustering
final List<LatLng> stores = [ ... ];
await map?.setClusters(stores);
3. Marker Opacity & Fade Animation (v0.2.0+) #
Eliminate the visual "flash" that occurs when clearing and re-adding markers by using the opacity parameter together with setMarkersOpacity() and a Flutter AnimationController.
How it works:
clearMarkers()β removes all old markers instantlyaddMarker(..., opacity: 0.0)β adds new markers invisiblysetMarkersOpacity(value)driven byAnimationControllerβ fades all markers in smoothly
// In your StatefulWidget, declare:
late final AnimationController _fadeCtrl;
@override
void initState() {
super.initState();
_fadeCtrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 350),
);
}
// Render markers with smooth fade-in:
Future<void> renderMarkersWithFade(List<LatLng> positions) async {
// Step 1: Clear old markers
await map?.clearMarkers();
// Step 2: Add all new markers invisibly
for (final pos in positions) {
await map?.addMarker(
pos,
color: '#E42320',
opacity: 0.0, // Start invisible
);
}
// Step 3: Fade in all markers together
_fadeCtrl.addListener(() {
map?.setMarkersOpacity(_fadeCtrl.value);
});
_fadeCtrl.forward(from: 0); // Animates 0.0 β 1.0 over 350ms
}
@override
void dispose() {
_fadeCtrl.dispose();
super.dispose();
}
Tip
setMarkersOpacity() updates all GL Circles and Symbols in a single batch β there is no per-marker loop required from the application side.
6. π₯ Camera & Viewport Control #
Directly manipulate the map viewport matching the fluidity of the Web SDK.
Movement Methods #
| Method | Arguments | Description |
|---|---|---|
moveCamera(target, ...) |
LatLng, zoom?, tilt?, bearing? |
Smooth cinematic fly-to animation with optional rotations. |
zoomIn() |
- | Increments zoom level by 1.0 with smooth animation. |
zoomOut() |
- | Decrements zoom level by 1.0 with smooth animation. |
setTilt(deg) |
double |
Set tilt angle (0 = 2D, 60+ = 3D). |
setBearing(deg) |
double |
Set direction (0 = North). |
tiltUp() |
- | Shortcut to animate the camera to a 3D perspective view. |
tiltDown() |
- | Shortcut to animate back to a top-down view. |
setCameraMode2D() |
- | (Navigator) Smooth transition to 2D top-down view (0Β° tilt) during navigation. |
setCameraMode3D() |
- | (Navigator) Smooth transition to 3D perspective view (70Β° tilt) during navigation. |
State Functions #
| Method | Returns | Description |
|---|---|---|
getCenter() |
LatLng? |
Returns current view center. |
getZoom() |
double? |
Returns current map zoom level. |
getBearing() |
double? |
Returns current map rotation. |
getTilt() |
double? |
Returns current camera pitch. |
await map?.moveCamera(
LatLng(13.7649, 100.5383),
zoom: 17.0,
tilt: 60.0,
bearing: 90.0,
);
7. π Search & Geocoding #
The Mobile SDK incorporates a fully custom GeocodingProvider interface. It mirrors the exact capabilities of the Web SDK while adding safety boundaries like Debouncing and LRU Caching.
Search Methods #
| Method | Arguments | Description |
|---|---|---|
search() |
query, location?, lang? |
Standard one-off search. Checks local LRU Cache first. |
searchByCategory() |
categoryId, location, {dist?, lang?} |
Searches for places nearby by category ID (e.g. '2100' = Restaurant). |
autocompleteDebounced() |
query, delay, onResult, location? |
Safe to bind to TextField.onChanged. Custom delay prevents quota burn. |
reverseGeocode() |
location, lang? |
Converts coordinates into a structured address (PowerMapSearchResult). |
clearSearchCache() |
- | Clears the internal LRU cache manually. |
Language Detection (lang Parameter) #
The lang parameter controls language script detection and search endpoint routing (v0.8.0+ defaults to 'auto'):
lang value |
Endpoint Strategy | Behavior |
|---|---|---|
'auto' (default) |
Dynamic Fallback | Auto-detects Thai/English script, tries primary endpoint, and falls back/merges secondary results deduplicated by gid. |
'th' |
/api/v2/map/address_th |
Target search for Thai location queries. |
'en' |
/api/v2/map/address_en |
Target search for English/Latin location queries. |
'mixed' |
/api/v2/map/address |
Queries combined endpoint for mixed Thai and English queries. |
Tip
v0.8.0+: With lang: 'auto', the SDK automatically inspects the input query script (Thai vs English), executes smart multi-endpoint fallback querying when appropriate, and merges results while eliminating duplicate records.
PowerMapSearchResult Model #
| Property | Type | Description |
|---|---|---|
name |
String |
Primary localized title of the location. |
nameTh |
String |
Explicit Thai name string. |
nameEn |
String |
Explicit English/Latin name string. |
address |
String |
Full formatted localized address string. |
addressTh |
String |
Formatted Thai address string (Tambon, Amphoe, Province). |
addressEn |
String |
Formatted English address string. |
position |
LatLng |
Exact geographic coordinates. |
province |
String |
Province / Changwat name. |
amphoe |
String |
District / Amphoe name. |
tambon |
String |
Sub-district / Tambon name. |
postCode |
String |
Postal code string. |
raw |
Map<String, dynamic> |
Raw API response payload. |
// Language is detected automatically in the demo:
String detectLang(String query) {
final hasLatin = query.runes.any((c) => (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A));
final hasThai = query.runes.any((c) => c >= 0x0E00 && c <= 0x0E7F);
if (hasLatin && !hasThai) return 'en';
if (hasLatin && hasThai) return 'mixed';
return 'th';
}
// 1. Forward search with auto-detected lang
final results = await map?.search(query, lang: detectLang(query));
// 2. Debounced Autocomplete (safe for TextField)
map?.autocompleteDebounced(
query,
Duration(milliseconds: 500),
(results) {
if (results.isNotEmpty) {
print("Found: ${results.first.name} at ${results.first.position}");
}
}
);
// 3. Reverse Geocode (Lat/Lng to Address)
final address = await map?.reverseGeocode(LatLng(14.2558, 100.9810));
if (address != null) {
print("Address: ${address.name}, ${address.province}");
}
// 4. Category Search (Nearby POIs by type)
final myLocation = await map?.getCurrentLocation();
if (myLocation != null) {
final restaurants = await map?.searchByCategory('2100', myLocation, dist: 5);
print("Found ${restaurants?.length} restaurants nearby!");
}
8. π£οΈ Routing & Optimization (map.routing) #
Calculate logistics-grade paths containing step-by-step metadata.
addRoute() Options #
| Option | Type | Default | Description |
|---|---|---|---|
start |
LatLng |
Required | Origin coordinate. |
destination |
LatLng |
Required | Final destination coordinate. |
waypoints |
List<LatLng> |
[] |
Intended stops between origin and destination. |
optimize |
bool |
false |
If true, applies TSP logic to reorder waypoints for efficiency. Minimum 1 waypoint required. |
profile |
String |
'driving' |
Transportation mode: 'driving', 'motorcycling', 'foot', 'cycling'. |
language |
String |
'th' |
Return instructions in 'th' or 'en'. |
PowerMapRouteResult Object #
| Property | Type | Description |
|---|---|---|
distance |
double |
Route length in meters. |
duration |
double |
Estimated seconds en route. |
distanceStr |
String |
Human-readable (e.g. "5.2 km"). |
durationStr |
String |
Human-readable (e.g. "12 mins"). |
steps |
List<PowerRouteStep> |
Step-by-step turn objects (contains text, distanceStr, location). |
geometry |
List<LatLng> |
Raw polyline coordinates array. |
profile |
String |
Active transportation mode profile (e.g. 'driving', 'motorcycling', 'foot', 'cycling'). |
final route = await map?.routing.addRoute(
start: LatLng(13.7468, 100.5346),
destination: LatLng(13.7649, 100.5383),
optimize: true
);
print("Route calculated: ${route?.distanceStr}");
Note
v0.3.0+: Route lines are rendered as GeoJSON layers positioned below road labels for improved visibility.
v0.8.0+: Turn maneuver guidance arrows are dynamically drawn directly on the active navigation polyline via drawStepTurnArrows(). Featuring 3-layer zoom-level interpolation (Casing line, White core line, Arrowhead symbol layer), the arrows dynamically scale across zoom levels matching standard navigation aesthetics.
9. π Geometry & Vector Data #
The Mobile SDK provides a robust API for handling GeoJSON data. You can import, render, and retrieve your map features with ease.
π Geometry API Reference #
| Method | Arguments | Description |
|---|---|---|
getGeoJSON() |
String? id, Map? opts |
The primary way to retrieve your data. Returns feature(s) as a Map object. |
exportGeometry() |
String? id, Map? opts |
Alias for getGeoJSON() for parity with the Web SDK. |
addGeoJson() |
sourceId, geoJsonMap |
Injects raw GeoJSON into the map source for later styling. |
importGeoJson() |
sourceId, geoJsonMap, ... |
Injects raw GeoJSON into the map and optionally renders it. |
addIndoorExtrusionLayer() |
sourceId, {layerId} |
Automatically renders 3D walls using the height property. |
addPolygonLayer() |
sourceId, {layerId} |
Draws standard 2D shapes (Fills and Outlines). Perfect for Geofences. |
addPolylineLayer() |
sourceId, {layerId} |
Renders paths and lines with scalable widths (LineString). |
π Code Example: Data Retrieval #
// 1. Get ALL geometry current in the session
final allFeatures = await map?.getGeoJSON();
// 2. Get a SPECIFIC feature by ID with format options
final specificFeature = await map?.getGeoJSON(
'poi-123',
options: { 'format': 'standard' }
);
π₯ Code Example: Data Ingestion & Layer Creation #
// 1. Prepare your Geometry Data (GeoJSON format: Lng, Lat)
final geofenceData = {
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[100.534, 13.746], [100.538, 13.746],
[100.538, 13.749], [100.534, 13.749],
[100.534, 13.746] // Must close the loop
]]
}
};
// 2. Inject Data into the Map Source
await map?.geometry.addGeoJson("my-geofence", geofenceData);
// 3. Apply Visual Styles to that Source ID
await map?.geometry.addPolygonLayer(
"my-geofence",
fillColor: "#4264fb",
fillOpacity: 0.5,
outlineColor: "#000000"
);
10. π’ Indoor Mapping (IMDF) #
The Mobile SDK natively supports the OGC Indoor Mapping Data Format (IMDF), allowing you to seamlessly render and navigate complex indoor environments like malls, airports, and hospitals.
The IndoorManager (map.indoor) provides the facade for managing venue states and floor switching logic.
ποΈ Loading an Indoor Map #
To initialize indoor mapping, first import the IMDF GeoJSON and tell the geometry manager to process it as imdf. Then, use the IndoorManager to switch to a specific venue and floor.
// 1. Load IMDF Data
final geoJsonString = await rootBundle.loadString('assets/sample_imdf.json');
final geoJson = jsonDecode(geoJsonString);
// 2. Import and set format to 'imdf'
await map?.importGeoJson(
'indoor_source',
geoJson,
options: {'format': 'imdf'}
);
// 3. Load the venue and start at floor 1
await map?.indoor.loadIndoorMap('venue_001', initialFloor: 1);
ποΈ Floor Switching & State #
| Method | Description |
|---|---|
loadIndoorMap(venueId, {initialFloor}) |
Sets the active venue and displays the requested floor. |
setFloor(level) |
Changes the currently visible floor. Automatically hides other floors. |
unloadIndoorMap() |
Clears the active indoor state and shows all geometry. |
// Switch to floor 2
await map?.indoor.setFloor(2);
// Check current state
print("Current Venue: ${map?.indoor.currentVenue}");
print("Current Floor: ${map?.indoor.currentFloor}");
π Dynamic Floor Filtering (v0.3.0+) #
Instead of re-importing data on each floor change, use setLayerFilter() to toggle visibility:
// Show only floor 2
await map?.geometry.setLayerFilter('indoor_source', [
'==', ['get', 'user_floor'], 2
]);
// Show all floors (clear filter)
await map?.geometry.setLayerFilter('indoor_source', null);
Note
v0.3.0+: IMDF conversion is now fully active. getGeoJSON() correctly converts between internal and IMDF formats. Venue and building features are now classified with their correct kind values instead of being misclassified as 'floor'.
π Querying Indoor Data #
The IndoorManager allows you to extract structural data from the loaded IMDF geometry, making it easy to build UI components like a Floor Switcher.
| Method | Returns | Description |
|---|---|---|
getVenues() |
Future<List<Map>> |
Get a list of all venues. |
getBuildings() |
Future<List<Map>> |
Get a list of all buildings. |
getVenueInfo([venueId]) |
Future<Map?> |
Returns venue metadata and a list of its available floors (floors). |
getFloorInfo(ordinal) |
Future<Map?> |
Returns detailed features on a specific floor (units, anchors, POIs). |
// Fetch the list of available floors for the current venue
final venueInfo = await map?.indoor.getVenueInfo();
if (venueInfo != null) {
List<int> availableFloors = venueInfo['floors'];
print("Available Floors: $availableFloors");
}
π Native POI Icons (IconUtil) #
To display Points of Interest (POIs) such as restrooms, elevators, and stores smoothly at 60FPS without Flutter widget overhead, the SDK leverages MapLibre's native Symbol Layer combined with IconUtil.
Warning
Architectural Standard: Developers MUST use the SDK's IconUtil for rendering IMDF FontAwesome POIs. Do NOT create custom Flutter Widgets or separate icon parsers in your app, as it severely impacts 60FPS performance and violates the Indoor SDK standards.
IconUtil dynamically converts FontAwesome icons into native Uint8List bitmaps with a premium App-Icon style (rounded rectangle, drop shadow, white border). You can also dynamically colorize them by reading properties from your IMDF data (e.g., _internal.icon_color).
import 'package:powermap_sdk/utils/icon_util.dart';
// 1. Extract icon name and color from your IMDF feature properties
final String iconName = feature.properties['_internal']['user_icon'] ?? 'restroom';
final String hexColor = feature.properties['_internal']['icon_color'] ?? '#B47FF7';
// 2. Generate the bitmap from the string name & parsed color
final bytes = await IconUtil.createBitmapFromIcon(
icon: IconUtil.getIconData(iconName),
color: IconUtil.parseColor(hexColor, defaultColor: const Color(0xFFB47FF7)),
size: 64.0, // High-res for canvas rendering
);
// 3. Create a compound ID and register it with the map's native engine
final String imageId = '${iconName}_$hexColor';
await map?.mapController.addImage(imageId, bytes);
// 4. Update your GeoJSON feature to reference the new ID
// feature.properties['_internal']['user_icon'] = imageId;
// Now it will automatically render on the map with the correct color!
11. π§ Real-Time Tracking #
Powered by Geolocator for battery-efficient, high-accuracy GPS positioning. Designed for delivery apps and navigation use cases without burning battery on heavy Dart timers.
Tracking Modes #
| Mode | Description |
|---|---|
TrackingMode.none |
Camera is unlocked β user can pan freely. |
TrackingMode.follow |
Camera follows device location smoothly. |
TrackingMode.followWithCompass |
Camera follows + rotates to match device heading (3D navigation view). |
Core Methods #
| Method | Returns | Description |
|---|---|---|
enableFollowMode() |
Future<void> |
Locks camera smoothly on device GPS location. Auto-requests permission. |
enableFollowModeWithCompass() |
Future<void> |
Locks camera + rotates map to match device heading with 3D tilt. |
disableFollowMode() |
Future<void> |
Unlocks camera, stops GPS stream to conserve battery. |
getCurrentLocation() |
Future<LatLng?> |
One-shot GPS request. Useful for "My Location" button. |
requestPermission() |
Future<bool> |
Checks & requests location permission. Returns true if granted. |
Usage Examples #
// 1. Follow device location smoothly
await map?.enableFollowMode();
// 2. Navigation mode with compass heading (3D view)
await map?.enableFollowModeWithCompass();
// 3. Stop tracking to save battery
await map?.disableFollowMode();
// 4. One-shot: Get current coordinates
final myLocation = await map?.getCurrentLocation();
if (myLocation != null) {
print("I'm at: $myLocation");
}
// 5. Listen to position updates reactively
map?.tracking.positionNotifier.addListener(() {
final pos = map?.tracking.positionNotifier.value;
if (pos != null) {
print("Lat: ${pos.latitude}, Lng: ${pos.longitude}");
}
});
Important
Battery Efficiency: The GPS stream uses distanceFilter: 5 meters β updates only fire when the device actually moves β₯5m. This prevents unnecessary CPU/battery drain compared to timer-based polling.
Note
Permissions: enableFollowMode() and enableFollowModeWithCompass() automatically request location permissions. You can also call requestPermission() manually during onboarding to pre-ask the user.
12. π Turn-by-Turn Navigation & Simulation (PowerMapNavigator) #
The SDK includes a fully-featured, 60FPS native-feeling Navigation Engine that maintains parity with the PowerMap Android/Kotlin SDK. It supports real-time GPS driving, dynamic route simulation, smooth 3D camera tracking, and Voice Guidance (TTS).
Core Features #
- Real-Time Speed Limit API (v0.8.0+): Seamless integration with PowerMap Speed Limit API via
SpeedLimitServicefeaturing 4-layer request throttling (Road Segment IDogc_fidcache, 30m distance filter, 4s time cooldown, in-flight lock). - Dynamic Step Turn Arrows (v0.8.0+): Real-time maneuver turn guidance arrows drawn on the navigation route line (
drawStepTurnArrows) with 3-layer zoom-level interpolation. - Enhanced Camera Experience (2D/3D & Smart Offset): Support for 3D map tilting, speed-adaptive camera bearing, and smart camera offset (
_offsetCameraTarget) to extend forward road visibility. - Smart Arrival Detection & Post-Navigation: Accurate destination arrival detection (available in both GPS and Simulation modes) with a seamless transition to a trip summary screen.
- Voice Guidance & Audio Ducking: Full support for localized Text-to-Speech (TTS) with 3-stage distance-based pre-announcements (e.g., 500m, 200m, and maneuver point). Includes Audio Ducking to lower background music during announcements.
- Advanced Navigation Engine:
- Noise Reduction & Precision: Incorporates a custom GPS filtering system and speed-adaptive accuracy thresholds to ignore bad GPS signals gracefully.
- Monotonic Progress Tracking: Prevents erratic vehicle snapping on complex road segments (e.g., parallel roads or U-turns).
- Background Operations:
- Designed to support continuous navigation even when the app is in the background.
- Utilizes Android Foreground Services and iOS Background Audio/Location modes.
- Built-in screen wake-lock to prevent the device from sleeping during active navigation.
- High-Performance 2D Symbols: Renders the vehicle using native GL Symbol layers (
iconSize: 1.0with sharp Miter joins) to prevent flickering and guarantee hardware-accelerated drawing.
Getting Started #
// 1. Initialize the Navigator
final navigator = PowerMapNavigator(controller: mapController);
// 2. Fetch a Route
final route = await mapController.routing.addRoute(
start: LatLng(13.7468, 100.5346),
destination: LatLng(13.7649, 100.5383),
profile: 'driving',
);
// 3. Pre-warm GPS during Route Preview (Recommended for instant Google Maps-style startup)
await navigator.warmUpGps();
// 4. Start Navigation (GPS Mode with Map Matching)
await navigator.startNavigation(route, mode: PowerNavigationMode.gps);
// OR Start Simulation Mode for testing
// await navigator.startNavigation(route, mode: PowerNavigationMode.simulation);
Real GPS Navigation & Pre-Warming #
To achieve instant, zero-delay navigation startup similar to Google Maps, call navigator.warmUpGps() when entering the route selection or route preview screen. This pre-warms the hardware location stream so position and bearing are ready in memory before starting navigation:
// Pre-warm GPS during route preview
await navigator.warmUpGps();
// Start GPS Navigation instantly without location fix delay or position warp
await navigator.startNavigation(route, mode: PowerNavigationMode.gps);
Speed Limit API & Throttling & SpeedLimitResult #
The SDK integrates SpeedLimitService to query real-time road speed limits (/api/v2/map/speed-limit?lat={lat}&lon={lon}). PowerMapNavigator automatically updates NavigationState.speedLimit and NavigationState.speedLimitResult using a 4-layer throttling engine (Road Segment ID ogc_fid cache, 30m distance threshold, 4s time cooldown, and in-flight request lock).
// Standalone Service Query
final speedService = SpeedLimitService();
final SpeedLimitResult? result = await speedService.getSpeedLimit(LatLng(14.334397, 100.613304));
print('Speed Limit: ${result?.speed} km/h, Road Segment ID: ${result?.ogcFid}');
// Active Navigation State Listening
navigator.stateNotifier.addListener(() {
final state = navigator.stateNotifier.value;
if (state?.speedLimitResult != null) {
final res = state!.speedLimitResult!;
print('Speed limit: ${res.speed} km/h (Road ID: ${res.ogcFid})');
}
});
SpeedLimitResult Properties
| Property | Type | Description |
|---|---|---|
speed |
double? |
Legal speed limit in km/h (e.g. 120, 90, 80, 50). |
distM |
double? |
Distance in meters to matched road feature. |
input |
LatLng? |
Original query coordinate input. |
ogcFid |
int? |
GIS database feature ID (ogc_fid) representing the road segment. |
debugCached |
bool? |
true if the result was served from local cache. |
raw |
Map<String, dynamic>? |
Raw JSON payload from the Speed Limit API. |
GPS Map Matching (Snap-to-Route) #
When running in PowerNavigationMode.gps, the PowerMap SDK utilizes robust Map Matching (snapToRoute) mathematics. As location updates are received from the physical hardware, the SDK automatically orthogonally projects the coordinates directly onto the geometry of the active route. This perfectly mitigates GPS jitter inside buildings or urban canyons and ensures accurate distance remaining, maneuver detection, and correct triggering of Voice Instructions.
Simulation Controls #
The simulation mode allows you to control the playback of the route for testing and demo purposes:
| Method | Description |
|---|---|
pauseSimulation() |
Halts the vehicle and camera movement instantly. |
resumeSimulation() |
Resumes movement from the paused position. |
setSimulationSpeed(multiplier) |
Adjusts the playback speed (e.g., 1X, 2X, 3X). |
setTracking(bool) |
Toggles camera tracking to follow the vehicle. |
stopNavigation() |
Kills the ticker, resets camera to Top-Down North (tilt 0), and clears markers. |
Camera Mode (2D/3D) (v0.3.0+) #
Toggle between a top-down 2D view and an immersive 3D perspective during active navigation:
| Method | Description |
|---|---|
setCameraMode2D() |
Smooth transition to 0Β° tilt. All subsequent camera updates maintain 2D. |
setCameraMode3D() |
Smooth transition to 70Β° tilt. Default 3D perspective. |
is2DMode |
Returns true if currently in 2D mode. |
// Toggle example
if (navigator.is2DMode) {
navigator.setCameraMode3D();
} else {
navigator.setCameraMode2D();
}
Navigation Language (v0.3.0+) #
Ensure all voice guidance and reroute instructions match your app's active language:
// Set before or during navigation
navigator.setNavigationLanguage('th'); // Thai
navigator.setNavigationLanguage('en'); // English
This updates both the routing API language parameter and the TTS engine locale in a single call.
Vehicle Customization & Interaction #
You can change the color of the navigation arrow on the fly, and even detect when the user taps on it!
// 1. Change vehicle color dynamically (e.g., via a Color Picker)
await navigator.setVehicleColor(Colors.blue);
// 2. Detect when the user taps the moving vehicle icon
navigator.onVehicleTapped = () {
print("Vehicle icon tapped! Show a menu!");
};
Binding to the UI #
React to real-time navigation changes using the NavigationState object:
ValueListenableBuilder<NavigationState?>(
valueListenable: navigator.stateNotifier,
builder: (context, state, child) {
if (state == null) return const SizedBox.shrink();
return Column(
children: [
Text(state.instructionText), // 'Turn left onto Sukhumvit Rd'
Text('${state.distanceToNextManeuver} m'), // '500.0 m'
Text('Speed: ${state.speedMps * 3.6} km/h'), // Current driven speed
if (state.estimatedArrivalTime != null)
Text('ETA: ${state.estimatedArrivalTime}'), // Estimated time of arrival
if (state.hasArrived)
Text('You have reached your destination!'), // Arrival status
],
);
},
);
Required Permissions Setup #
To fully utilize Navigation 2.0.1's background capabilities, ensure your app has the correct permissions configured:
Android
Add the following to your AndroidManifest.xml:
- Foreground Service permission for background navigation.
- Background Location permission for continuous tracking.
iOS
Add the following to your Info.plist:
- Enable
UIBackgroundModeswithaudio(for background TTS) andlocation(for background tracking).
13. π¨ UI & UX Best Practices #
The Facade Pattern #
For maximum DX, you do not need to dive into individual managers. All primary actions (search, addMarker, addRoute, moveCamera, setLayers, getGeoJSON) are available directly on the PowerMapController instance:
map?.search("Mall");
map?.getGeoJSON("poi-123");
map?.addRoute(...);
14. π Build & Development (For Contributors) #
To compile the SDK:
- Clone Repository.
flutter pub get.- Check
lib/managers/for logic separation. - Run
dart analyzeto ensure code meets standards.
15. π‘οΈ Error Handling System #
The SDK provides a unified PowerMapException class for handling all types of failures. Instead of generic errors, you get specific codes and descriptive messages.
PowerMapException Properties #
| Property | Type | Description |
|---|---|---|
code |
String |
Programmatic error code (e.g., NETWORK_ERROR). |
message |
String |
Human-readable explanation in English. |
details |
dynamic |
The underlying raw error (Stacktrace or Response body). |
Common Error Codes #
| Code | Triggered By | Reason |
|---|---|---|
AUTH_FAILED |
Any API | Missing/Incorrect mapApiKey or clientSecret. |
NETWORK_ERROR |
Any API | No internet connection or server unreachable. |
STYLE_LOAD_FAILED |
Map View | Failed to load the optimized style JSON. |
ROUTE_NOT_FOUND |
Routing | No path exists between the selected points. |
SERVER_ERROR |
Any API | Backend returned a 500 error. |
Handling Errors in Code #
try {
final results = await map.search("Siam Paragon");
} on PowerMapException catch (e) {
if (e.code == PowerMapException.network) {
print("Please check your connection!");
} else {
print("PowerMap Error: ${e.message}");
}
}
Β© 2026 PowerMap Development Team. All rights reserved.