chore: add build artifacts, map assets, and routing data

This commit is contained in:
Hamza-Ayed
2026-09-19 12:34:24 +03:00
parent aa2b9f131f
commit 43ad2e0ad4
327 changed files with 52421 additions and 1810 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -48,6 +48,9 @@ class IntaleqMapController {
/// destroyed) so the next update re-adds it.
mgl.Symbol? _userSymbol;
bool _userSymbolBusy = false;
bool _isStyleLoaded = false;
CameraUpdate? _pendingCameraUpdate;
bool _pendingCameraAnimated = true;
// ── Factory / init ─────────────────────────────────────────
@@ -75,6 +78,7 @@ class IntaleqMapController {
/// We clear internal registries because native objects (Symbols, Lines)
/// are destroyed on style reload.
Future<void> onStyleLoaded() async {
_isStyleLoaded = true;
_symbols.clear();
_symbolToMarker.clear();
_lines.clear();
@@ -86,6 +90,24 @@ class IntaleqMapController {
// stale handle so the next [setUserMarker] call re-creates it.
_userSymbol = null;
await _registerDefaultImages();
// Safely execute camera updates queued before style finished rendering
if (_pendingCameraUpdate != null) {
final pending = _pendingCameraUpdate!;
final animated = _pendingCameraAnimated;
_pendingCameraUpdate = null;
print("🎬 [IntaleqController] Style loaded! Flushing queued camera update (animated=$animated)...");
try {
if (animated) {
await _raw.animateCamera(pending.toMapLibre());
} else {
await _raw.moveCamera(pending.toMapLibre());
}
print("✅ [IntaleqController] Queued camera update executed successfully.");
} catch (e) {
print("⚠️ [IntaleqController] Queued camera update trapped error: $e");
}
}
}
/// Google Maps draws every marker unconditionally, but MapLibre's symbol
@@ -113,13 +135,43 @@ class IntaleqMapController {
// ── Camera (same API as GoogleMapController) ──────────────
/// Animates the camera to the given [update].
Future<bool?> animateCamera(CameraUpdate update) =>
_raw.animateCamera(update.toMapLibre());
/// Animates the camera to the given [update]. Safe on iOS before style is loaded.
Future<bool?> animateCamera(CameraUpdate update) async {
if (!_isStyleLoaded) {
print("⏳ [IntaleqController] animateCamera: Style not loaded yet. Queuing camera update safely.");
_pendingCameraUpdate = update;
_pendingCameraAnimated = true;
return null;
}
try {
print("🎬 [IntaleqController] animateCamera: Executing native animation now.");
return await _raw.animateCamera(update.toMapLibre());
} catch (e) {
print("⚠️ [IntaleqController] animateCamera native call error: $e. Re-queuing.");
_pendingCameraUpdate = update;
_pendingCameraAnimated = true;
return null;
}
}
/// Instantly moves the camera to the given [update].
Future<bool?> moveCamera(CameraUpdate update) =>
_raw.moveCamera(update.toMapLibre());
/// Instantly moves the camera to the given [update]. Safe on iOS before style is loaded.
Future<bool?> moveCamera(CameraUpdate update) async {
if (!_isStyleLoaded) {
print("⏳ [IntaleqController] moveCamera: Style not loaded yet. Queuing camera update safely.");
_pendingCameraUpdate = update;
_pendingCameraAnimated = false;
return null;
}
try {
print("🎬 [IntaleqController] moveCamera: Executing native move now.");
return await _raw.moveCamera(update.toMapLibre());
} catch (e) {
print("⚠️ [IntaleqController] moveCamera native call error: $e. Re-queuing.");
_pendingCameraUpdate = update;
_pendingCameraAnimated = false;
return null;
}
}
/// Returns the current [CameraPosition] of the map.
CameraPosition? get cameraPosition => _raw.cameraPosition != null
@@ -150,23 +202,35 @@ class IntaleqMapController {
}
/// Adds a single [Marker] to the map and returns its MapLibre handle.
Future<mgl.Symbol> addMarker(Marker marker) async {
final symbol = await _raw.addSymbol(marker.toSymbolOptions());
_symbols[marker.markerId] = symbol;
_symbolToMarker[symbol.id] = marker;
Future<mgl.Symbol?> addMarker(Marker marker) async {
if (!_isStyleLoaded) return null;
try {
await _loadBitmapIfNeeded(marker.icon);
final symbol = await _raw.addSymbol(marker.toSymbolOptions());
_symbols[marker.markerId] = symbol;
_symbolToMarker[symbol.id] = marker;
// Ensure collision defaults apply (symbol layer is created lazily by MapLibre)
await applyMarkerVisibilityDefaults();
return symbol;
// Ensure collision defaults apply (symbol layer is created lazily by MapLibre)
await applyMarkerVisibilityDefaults();
return symbol;
} catch (e) {
print("⚠️ [IntaleqMapController] addMarker failed for ${marker.markerId.value}: $e");
return null;
}
}
/// Updates an existing marker's position / appearance.
Future<void> _updateMarker(Marker marker) async {
if (!_isStyleLoaded) return;
final symbol = _symbols[marker.markerId];
if (symbol == null) return;
await _raw.updateSymbol(symbol, marker.toSymbolOptions());
_symbolToMarker[symbol.id] = marker;
try {
await _loadBitmapIfNeeded(marker.icon);
await _raw.updateSymbol(symbol, marker.toSymbolOptions());
_symbolToMarker[symbol.id] = marker;
} catch (e) {
print("⚠️ [IntaleqMapController] _updateMarker failed for ${marker.markerId.value}: $e");
}
}
/// Removes a marker by its [MarkerId].
@@ -174,7 +238,9 @@ class IntaleqMapController {
final symbol = _symbols.remove(id);
if (symbol == null) return;
_symbolToMarker.remove(symbol.id);
await _raw.removeSymbol(symbol);
try {
await _raw.removeSymbol(symbol);
} catch (_) {}
}
// ── User-location puck (imperative, high-frequency) ────────
@@ -192,7 +258,7 @@ class IntaleqMapController {
/// dropped, and the next tick supplies fresh coordinates). The puck is
/// re-created automatically after a style reload.
Future<void> setUserMarker(Marker marker) async {
if (_userSymbolBusy) return;
if (!_isStyleLoaded || _userSymbolBusy) return;
_userSymbolBusy = true;
try {
await _loadBitmapIfNeeded(marker.icon);
@@ -202,6 +268,7 @@ class IntaleqMapController {
} else {
await _raw.updateSymbol(symbol, marker.toSymbolOptions());
}
} catch (_) {
} finally {
_userSymbolBusy = false;
}
@@ -212,68 +279,96 @@ class IntaleqMapController {
final symbol = _userSymbol;
if (symbol == null) return;
_userSymbol = null;
await _raw.removeSymbol(symbol);
try {
await _raw.removeSymbol(symbol);
} catch (_) {}
}
// ── Polylines ──────────────────────────────────────────────
Future<mgl.Line> addPolyline(Polyline polyline) async {
final line = await _raw.addLine(polyline.toLineOptions());
_lines[polyline.polylineId] = line;
_lineToPolyline[line.id] = polyline;
return line;
Future<mgl.Line?> addPolyline(Polyline polyline) async {
if (!_isStyleLoaded) return null;
try {
final line = await _raw.addLine(polyline.toLineOptions());
_lines[polyline.polylineId] = line;
_lineToPolyline[line.id] = polyline;
return line;
} catch (_) {
return null;
}
}
Future<void> _updatePolyline(Polyline polyline) async {
if (!_isStyleLoaded) return;
final line = _lines[polyline.polylineId];
if (line == null) return;
await _raw.updateLine(line, polyline.toLineOptions());
_lineToPolyline[line.id] = polyline;
try {
await _raw.updateLine(line, polyline.toLineOptions());
_lineToPolyline[line.id] = polyline;
} catch (_) {}
}
Future<void> _removePolyline(PolylineId id) async {
final line = _lines.remove(id);
if (line == null) return;
_lineToPolyline.remove(line.id);
await _raw.removeLine(line);
try {
await _raw.removeLine(line);
} catch (_) {}
}
// ── Circles ────────────────────────────────────────────────
Future<void> addCircle(Circle circle) async {
final c = await _raw.addCircle(circle.toCircleOptions());
_circles[circle.circleId] = c;
if (!_isStyleLoaded) return;
try {
final c = await _raw.addCircle(circle.toCircleOptions());
_circles[circle.circleId] = c;
} catch (_) {}
}
Future<void> _updateCircle(Circle circle) async {
if (!_isStyleLoaded) return;
final c = _circles[circle.circleId];
if (c == null) return;
await _raw.updateCircle(c, circle.toCircleOptions());
try {
await _raw.updateCircle(c, circle.toCircleOptions());
} catch (_) {}
}
Future<void> _removeCircle(CircleId id) async {
final c = _circles.remove(id);
if (c == null) return;
await _raw.removeCircle(c);
try {
await _raw.removeCircle(c);
} catch (_) {}
}
// ── Polygons ───────────────────────────────────────────────
Future<void> addPolygon(Polygon polygon) async {
final fill = await _raw.addFill(polygon.toFillOptions());
_fills[polygon.polygonId] = fill;
if (!_isStyleLoaded) return;
try {
final fill = await _raw.addFill(polygon.toFillOptions());
_fills[polygon.polygonId] = fill;
} catch (_) {}
}
Future<void> _updatePolygon(Polygon polygon) async {
if (!_isStyleLoaded) return;
final fill = _fills[polygon.polygonId];
if (fill == null) return;
await _raw.updateFill(fill, polygon.toFillOptions());
try {
await _raw.updateFill(fill, polygon.toFillOptions());
} catch (_) {}
}
Future<void> _removePolygon(PolygonId id) async {
final fill = _fills.remove(id);
if (fill == null) return;
await _raw.removeFill(fill);
try {
await _raw.removeFill(fill);
} catch (_) {}
}
// ── Tap routing ────────────────────────────────────────────
@@ -294,6 +389,7 @@ class IntaleqMapController {
Set<Marker> oldSet,
Set<Marker> newSet,
) async {
if (!_isStyleLoaded) return;
final oldMap = {for (final m in oldSet) m.markerId: m};
final newMap = {for (final m in newSet) m.markerId: m};
@@ -316,6 +412,7 @@ class IntaleqMapController {
Set<Polyline> oldSet,
Set<Polyline> newSet,
) async {
if (!_isStyleLoaded) return;
final oldMap = {for (final p in oldSet) p.polylineId: p};
final newMap = {for (final p in newSet) p.polylineId: p};
@@ -335,6 +432,7 @@ class IntaleqMapController {
Set<Circle> oldSet,
Set<Circle> newSet,
) async {
if (!_isStyleLoaded) return;
final oldMap = {for (final c in oldSet) c.circleId: c};
final newMap = {for (final c in newSet) c.circleId: c};
@@ -354,6 +452,7 @@ class IntaleqMapController {
Set<Polygon> oldSet,
Set<Polygon> newSet,
) async {
if (!_isStyleLoaded) return;
final oldMap = {for (final p in oldSet) p.polygonId: p};
final newMap = {for (final p in newSet) p.polygonId: p};
@@ -484,13 +583,15 @@ class IntaleqMapController {
if (bitmap.bytes != null) {
await _raw.addImage(id, bitmap.bytes!);
_loadedImages.add(id);
print("🖼️ [IntaleqMapController] Registered custom marker bitmap from bytes: $id");
} else if (bitmap.assetName != null) {
try {
final data = await rootBundle.load(bitmap.assetName!);
await _raw.addImage(id, data.buffer.asUint8List());
_loadedImages.add(id);
} catch (_) {
// Asset not found — marker will fall back to default.
print("🖼️ [IntaleqMapController] Registered custom marker asset: ${bitmap.assetName} as $id");
} catch (e) {
print("⚠️ [IntaleqMapController] Failed to load asset image ${bitmap.assetName}: $e");
}
}
// Style-registered images need no loading.
@@ -1,9 +1,9 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:maplibre_gl/maplibre_gl.dart' as mgl;
import 'intaleq_map_controller.dart';
import 'styles.dart';
import 'offline_service.dart';
import 'models/geometry.dart';
import 'models/types.dart';
@@ -59,7 +59,7 @@ class IntaleqMap extends StatefulWidget {
this.scrollGesturesEnabled = true,
this.tiltGesturesEnabled = true,
this.zoomGesturesEnabled = true,
this.autoCache = true,
this.autoCache = false,
this.minMaxZoomPreference = MinMaxZoomPreference.unbounded,
this.cameraTargetBounds = CameraTargetBounds.unbounded,
});
@@ -144,6 +144,7 @@ class _IntaleqMapState extends State<IntaleqMap> {
bool _isCameraMoving = false;
String? _styleString;
bool _isLoadingStyle = true;
bool _isNativeStyleLoaded = false;
@override
void initState() {
@@ -158,10 +159,20 @@ class _IntaleqMapState extends State<IntaleqMap> {
if (url.startsWith('{')) {
_styleString = url;
} else if (url.startsWith('asset://')) {
} else if (url.startsWith('asset://') || url.startsWith('assets/') || (url.endsWith('.json') && !url.startsWith('http'))) {
final assetPath = url.replaceFirst('asset://', '');
_styleString = await rootBundle.loadString(assetPath);
print("🔥 [IntaleqMap] Style loaded from asset string: ${assetPath.split('/').last}");
try {
_styleString = await rootBundle.loadString(assetPath);
print("🔥 [IntaleqMap] Style loaded from asset string: ${assetPath.split('/').last}");
} catch (_) {
try {
_styleString = await rootBundle.loadString('packages/intaleq_maps/$assetPath');
print("🔥 [IntaleqMap] Style loaded from package asset: packages/intaleq_maps/$assetPath");
} catch (e) {
print("❌ [IntaleqMap] Asset load failed: $e");
_styleString = url;
}
}
} else if (url.startsWith('file://')) {
final filePath = url.replaceFirst('file://', '');
final file = File(filePath);
@@ -171,9 +182,49 @@ class _IntaleqMapState extends State<IntaleqMap> {
} else {
_styleString = url;
}
} else if (url.startsWith('http://') || url.startsWith('https://')) {
// If satellite theme requested, use bundled ESRI Satellite hybrid style directly
if (widget.mapType == IntaleqMapType.satellite || url.contains('theme=satellite')) {
try {
_styleString = await rootBundle.loadString('packages/intaleq_maps/assets/style-satellite.json');
print("🛰️ [IntaleqMap] Loaded bundled ESRI Satellite hybrid style successfully");
return;
} catch (_) {
try {
_styleString = await rootBundle.loadString('assets/style-satellite.json');
print("🛰️ [IntaleqMap] Loaded local ESRI Satellite hybrid style successfully");
return;
} catch (_) {}
}
}
try {
final res = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 4));
if (res.statusCode == 200 && res.body.trim().startsWith('{')) {
_styleString = res.body;
print("🔥 [IntaleqMap] Pre-fetched remote style JSON (${res.body.length} bytes)");
} else {
_styleString = url;
}
} catch (netErr) {
print("⚠️ [IntaleqMap] Direct HTTP fetch failed ($netErr), attempting bundled fallback");
try {
final fallback = (widget.mapType == IntaleqMapType.satellite || url.contains('theme=satellite'))
? 'packages/intaleq_maps/assets/style-satellite.json'
: 'packages/intaleq_maps/assets/style.json';
_styleString = await rootBundle.loadString(fallback);
print("🔥 [IntaleqMap] Loaded bundled offline fallback style successfully: $fallback");
} catch (_) {
_styleString = url;
}
}
} else {
_styleString = url;
print("🔥 [IntaleqMap] Using remote style URL: $url");
// Fallback: try rootBundle first
try {
_styleString = await rootBundle.loadString(url);
print("🔥 [IntaleqMap] Style loaded from rootBundle fallback: $url");
} catch (_) {
_styleString = url;
}
}
} catch (e) {
print("❌ [IntaleqMap] Failed to load style: $e");
@@ -191,11 +242,12 @@ class _IntaleqMapState extends State<IntaleqMap> {
if (oldWidget.mapType != widget.mapType ||
oldWidget.styleUrl != widget.styleUrl) {
_isNativeStyleLoaded = false;
_loadStyle();
}
final ctrl = _controller;
if (ctrl == null) return;
if (ctrl == null || !_isNativeStyleLoaded) return;
// Reconcile each overlay set when widget rebuilds.
ctrl.diffMarkers(oldWidget.markers, widget.markers);
@@ -206,17 +258,17 @@ class _IntaleqMapState extends State<IntaleqMap> {
String get _resolvedStyleUrl {
if (widget.styleUrl != null) return widget.styleUrl!;
// Point to live sovereign server style by default (Google + OSM modern hybrid style)
// Default to local assets for instant loading and 100% offline support (as in Siro Rider)
return switch (widget.mapType) {
IntaleqMapType.normal => IntaleqStyles.light(widget.apiKey),
IntaleqMapType.light => IntaleqStyles.light(widget.apiKey),
IntaleqMapType.satellite => IntaleqStyles.satellite(widget.apiKey),
IntaleqMapType.normal => 'asset://packages/intaleq_maps/assets/style_dark.json',
IntaleqMapType.light => 'asset://packages/intaleq_maps/assets/style.json',
IntaleqMapType.satellite => 'asset://packages/intaleq_maps/assets/style-satellite.json',
IntaleqMapType.none => 'about:blank',
};
}
Future<void> _onMapCreated(mgl.MapLibreMapController rawCtrl) async {
// Wire up tap routing before handing the controller to the caller.
print("🗺️ [IntaleqMapWidget] _onMapCreated: Native MapLibre controller attached.");
rawCtrl.onSymbolTapped.add(_onSymbolTapped);
rawCtrl.onLineTapped.add(_onLineTapped);
@@ -225,6 +277,7 @@ class _IntaleqMapState extends State<IntaleqMap> {
apiKey: widget.apiKey,
);
_controller = ctrl;
print("🗺️ [IntaleqMapWidget] Invoking widget.onMapCreated callback...");
widget.onMapCreated?.call(ctrl);
}
@@ -234,18 +287,22 @@ class _IntaleqMapState extends State<IntaleqMap> {
void _onLineTapped(mgl.Line line) => _controller?.onLineTapped(line);
Future<void> _onStyleLoaded() async {
print("🎨 [IntaleqMapWidget] _onStyleLoaded: MapLibre native style finished loading.");
_isNativeStyleLoaded = true;
final ctrl = _controller;
if (ctrl == null) return;
if (ctrl == null) {
print("⚠️ [IntaleqMapWidget] _onStyleLoaded: _controller is null!");
return;
}
await ctrl.onStyleLoaded();
print("🎨 [IntaleqMapWidget] Invoking widget.onStyleLoaded callback...");
final callbackResult = widget.onStyleLoaded?.call();
if (callbackResult is Future) {
await callbackResult;
}
// Re-render everything from the current declarative sets.
// This ensures overlays persist across style changes (Dark/Light mode)
// and during certain zoom/camera events that trigger style reloads.
print("📌 [IntaleqMapWidget] Re-rendering overlays: markers=${widget.markers.length}, polylines=${widget.polylines.length}, circles=${widget.circles.length}, polygons=${widget.polygons.length}");
for (final m in widget.markers) await ctrl.addMarker(m);
for (final p in widget.polylines) await ctrl.addPolyline(p);
for (final c in widget.circles) await ctrl.addCircle(c);
@@ -253,6 +310,12 @@ class _IntaleqMapState extends State<IntaleqMap> {
// Apply defaults AFTER markers have initialized the symbol layer
await ctrl.applyMarkerVisibilityDefaults();
// Safely refresh to enable native location layer after style is ready on iOS
if (widget.myLocationEnabled && mounted) {
print("📍 [IntaleqMapWidget] Enabling native location layer after style is loaded.");
setState(() {});
}
}
@override
@@ -261,7 +324,9 @@ class _IntaleqMapState extends State<IntaleqMap> {
return const Center(child: CircularProgressIndicator());
}
return mgl.MaplibreMap(
final bool canEnableLocation = _isNativeStyleLoaded && widget.myLocationEnabled;
return mgl.MapLibreMap(
styleString: _styleString!,
initialCameraPosition: widget.initialCameraPosition.toMapLibre(),
onMapCreated: _onMapCreated,
@@ -288,8 +353,8 @@ class _IntaleqMapState extends State<IntaleqMap> {
widget.onCameraIdle?.call();
},
onCameraTrackingChanged: null,
myLocationEnabled: widget.myLocationEnabled,
myLocationRenderMode: widget.myLocationEnabled
myLocationEnabled: canEnableLocation,
myLocationRenderMode: canEnableLocation
? mgl.MyLocationRenderMode.normal
: mgl.MyLocationRenderMode.normal,
myLocationTrackingMode: mgl.MyLocationTrackingMode.none,
@@ -62,8 +62,9 @@ class InlqBitmap {
/// icon: InlqBitmap.fromAsset('assets/icons/car.png')
/// ```
static InlqBitmap fromAsset(String assetName, {double? size}) {
final sanitizedId = 'asset_${assetName.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_')}';
return InlqBitmap._(
mapLibreImageId: 'asset:$assetName',
mapLibreImageId: sanitizedId,
assetName: assetName,
size: size,
);
@@ -196,7 +196,7 @@ class Marker {
iconAnchor: _anchorToString(anchor),
draggable: draggable,
zIndex: zIndex.toInt(),
textField: infoWindow.title,
textField: (infoWindow.snippet == 'show_label' && infoWindow.title != null) ? infoWindow.title : null,
textAnchor: 'bottom',
textOffset: const Offset(0, -3.0),
textSize: 12.0,
@@ -210,11 +210,7 @@ class Marker {
: (infoWindow.snippet == 'stop_1' ? '#9C27B0' // Purple
: '#FFFFFF'))),
textHaloWidth: 3.0,
// The bundled Intaleq styles serve glyphs from a host that only carries
// Noto Sans. Without an explicit font, the annotation layer falls back to
// "Open Sans Regular, Arial Unicode MS Regular"; that glyph request 404s
// and MapLibre then drops the entire symbol — icon included — on iOS.
fontNames: infoWindow.title != null ? const ['Noto Sans Regular'] : null,
fontNames: (infoWindow.snippet == 'show_label' && infoWindow.title != null) ? const ['Noto Sans Regular'] : null,
);
}
@@ -571,9 +567,12 @@ class Polygon {
/// Converts a Flutter [Color] to a CSS hex string (e.g. '#0D47A1').
String _colorToHex(Color color) {
return '#${color.red.toRadixString(16).padLeft(2, '0')}'
'${color.green.toRadixString(16).padLeft(2, '0')}'
'${color.blue.toRadixString(16).padLeft(2, '0')}';
final r = (color.r * 255.0).round().clamp(0, 255);
final g = (color.g * 255.0).round().clamp(0, 255);
final b = (color.b * 255.0).round().clamp(0, 255);
return '#${r.toRadixString(16).padLeft(2, '0')}'
'${g.toRadixString(16).padLeft(2, '0')}'
'${b.toRadixString(16).padLeft(2, '0')}';
}
// ─────────────────────────────────────────────────────────────
+9 -3
View File
@@ -26,9 +26,15 @@ class IntaleqStyles {
/// Path to the local dark style asset.
static const String localDark = 'assets/style_dark.json';
/// Sovereign 100% Offline Tactical / Military Style (Defaults to White / High-Contrast Light Style)
/// التصميم التكتيكي والعسكري الفاتح (الأبيض) للعمل الميداني والنهاري في وضع الأوفلاين
static String offlineTactical({String localStylePath = 'assets/style.json'}) =>
/// Path to the local ESRI satellite hybrid style asset.
static const String localSatellite = 'assets/style-satellite.json';
/// Path to the sovereign tactical military style asset with rock escarpments, berms, and cliffs.
static const String localTactical = 'assets/tactical-style.json';
/// Sovereign 100% Offline Tactical / Military Style (Defaults to Tactical Style with Obstacles)
/// التصميم التكتيكي والعسكري مع طبقات الموانع الصخرية والجدران والخنادق
static String offlineTactical({String localStylePath = 'assets/tactical-style.json'}) =>
localStylePath.startsWith('assets/') || localStylePath.startsWith('asset://')
? (localStylePath.startsWith('asset://') ? localStylePath : 'asset://$localStylePath')
: (localStylePath.startsWith('file://') ? localStylePath : 'file://$localStylePath');
+1
View File
@@ -27,3 +27,4 @@ flutter:
assets:
- assets/style.json
- assets/style_dark.json
- assets/style-satellite.json