chore: add build artifacts, map assets, and routing data
This commit is contained in:
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')}';
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -27,3 +27,4 @@ flutter:
|
||||
assets:
|
||||
- assets/style.json
|
||||
- assets/style_dark.json
|
||||
- assets/style-satellite.json
|
||||
|
||||
Reference in New Issue
Block a user