mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-07-09 22:18:38 +02:00
laggy on android, UX needs polish
This commit is contained in:
@@ -97,19 +97,14 @@ class DeflockTileImageProvider extends ImageProvider<DeflockTileImageProvider> {
|
||||
return await decode(buffer);
|
||||
|
||||
} catch (e) {
|
||||
// Log error for debugging but don't spam network status
|
||||
debugPrint('[DeflockTileProvider] Failed to load tile ${coordinates.z}/${coordinates.x}/${coordinates.y}: $e');
|
||||
// Don't log routine offline misses to avoid console spam
|
||||
if (!e.toString().contains('offline mode is enabled')) {
|
||||
debugPrint('[DeflockTileProvider] Failed to load tile ${coordinates.z}/${coordinates.x}/${coordinates.y}: $e');
|
||||
}
|
||||
|
||||
// Return a transparent 1x1 pixel tile for missing tiles
|
||||
// This is more graceful than throwing and prevents cascade failures
|
||||
final transparentPixel = Uint8List.fromList([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0B, 0x49, 0x44, 0x41,
|
||||
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
|
||||
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82]);
|
||||
|
||||
final buffer = await ImmutableBuffer.fromUint8List(transparentPixel);
|
||||
return await decode(buffer);
|
||||
// Re-throw the exception and let FlutterMap handle missing tiles gracefully
|
||||
// This is better than trying to provide fallback images
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class MapDataProvider {
|
||||
bounds: bounds,
|
||||
profiles: profiles,
|
||||
uploadMode: uploadMode,
|
||||
maxResults: AppState.instance.maxCameras,
|
||||
maxResults: 0, // No limit - fetch all available data
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ class MapDataProvider {
|
||||
return fetchLocalNodes(
|
||||
bounds: bounds,
|
||||
profiles: profiles,
|
||||
maxNodes: AppState.instance.maxCameras,
|
||||
maxNodes: 0, // No limit - get all available data
|
||||
);
|
||||
}
|
||||
} else if (uploadMode == UploadMode.sandbox) {
|
||||
@@ -86,7 +86,7 @@ class MapDataProvider {
|
||||
bounds: bounds,
|
||||
profiles: profiles,
|
||||
uploadMode: uploadMode,
|
||||
maxResults: AppState.instance.maxCameras,
|
||||
maxResults: 0, // No limit - fetch all available data
|
||||
);
|
||||
} else {
|
||||
// Production mode: use pre-fetch service for efficient area loading
|
||||
@@ -116,13 +116,9 @@ class MapDataProvider {
|
||||
debugPrint('[MapDataProvider] Using existing fresh pre-fetched area cache');
|
||||
}
|
||||
|
||||
// Apply rendering limit and warn if nodes are being excluded
|
||||
final maxNodes = AppState.instance.maxCameras;
|
||||
if (localNodes.length > maxNodes) {
|
||||
NetworkStatus.instance.reportNodeLimitReached(localNodes.length, maxNodes);
|
||||
}
|
||||
|
||||
return localNodes.take(maxNodes).toList();
|
||||
// Return all local nodes without any rendering limit
|
||||
// Rendering limits are applied at the UI layer
|
||||
return localNodes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'dart:async';
|
||||
import '../app_state.dart';
|
||||
|
||||
enum NetworkIssueType { overpassApi }
|
||||
enum NetworkStatusType { waiting, issues, timedOut, noData, ready, success, nodeLimitReached }
|
||||
enum NetworkStatusType { waiting, issues, timedOut, noData, ready, success }
|
||||
|
||||
|
||||
|
||||
@@ -22,9 +22,6 @@ class NetworkStatus extends ChangeNotifier {
|
||||
Timer? _waitingTimer;
|
||||
Timer? _noDataResetTimer;
|
||||
Timer? _successResetTimer;
|
||||
bool _nodeLimitReached = false;
|
||||
Timer? _nodeLimitResetTimer;
|
||||
|
||||
// Getters
|
||||
bool get hasAnyIssues => _overpassHasIssues;
|
||||
bool get overpassHasIssues => _overpassHasIssues;
|
||||
@@ -32,7 +29,6 @@ class NetworkStatus extends ChangeNotifier {
|
||||
bool get isTimedOut => _isTimedOut;
|
||||
bool get hasNoData => _hasNoData;
|
||||
bool get hasSuccess => _hasSuccess;
|
||||
bool get nodeLimitReached => _nodeLimitReached;
|
||||
|
||||
NetworkStatusType get currentStatus {
|
||||
// Simple single-path status logic
|
||||
@@ -41,7 +37,6 @@ class NetworkStatus extends ChangeNotifier {
|
||||
if (_isTimedOut) return NetworkStatusType.timedOut;
|
||||
if (_hasNoData) return NetworkStatusType.noData;
|
||||
if (_hasSuccess) return NetworkStatusType.success;
|
||||
if (_nodeLimitReached) return NetworkStatusType.nodeLimitReached;
|
||||
return NetworkStatusType.ready;
|
||||
}
|
||||
|
||||
@@ -144,17 +139,15 @@ class NetworkStatus extends ChangeNotifier {
|
||||
|
||||
/// Clear waiting/timeout/no-data status (legacy method for compatibility)
|
||||
void clearWaiting() {
|
||||
if (_isWaitingForData || _isTimedOut || _hasNoData || _hasSuccess || _nodeLimitReached) {
|
||||
if (_isWaitingForData || _isTimedOut || _hasNoData || _hasSuccess) {
|
||||
_isWaitingForData = false;
|
||||
_isTimedOut = false;
|
||||
_hasNoData = false;
|
||||
_hasSuccess = false;
|
||||
_nodeLimitReached = false;
|
||||
_recentOfflineMisses = 0;
|
||||
_waitingTimer?.cancel();
|
||||
_noDataResetTimer?.cancel();
|
||||
_successResetTimer?.cancel();
|
||||
_nodeLimitResetTimer?.cancel();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -195,22 +188,6 @@ class NetworkStatus extends ChangeNotifier {
|
||||
debugPrint('[NetworkStatus] Network error occurred');
|
||||
}
|
||||
|
||||
/// Show notification that node display limit was reached
|
||||
void reportNodeLimitReached(int totalNodes, int maxNodes) {
|
||||
_nodeLimitReached = true;
|
||||
notifyListeners();
|
||||
debugPrint('[NetworkStatus] Node display limit reached: $totalNodes found, showing $maxNodes');
|
||||
|
||||
// Auto-clear after 8 seconds
|
||||
_nodeLimitResetTimer?.cancel();
|
||||
_nodeLimitResetTimer = Timer(const Duration(seconds: 8), () {
|
||||
if (_nodeLimitReached) {
|
||||
_nodeLimitReached = false;
|
||||
notifyListeners();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Report that a tile was not available offline
|
||||
@@ -243,7 +220,6 @@ class NetworkStatus extends ChangeNotifier {
|
||||
_waitingTimer?.cancel();
|
||||
_noDataResetTimer?.cancel();
|
||||
_successResetTimer?.cancel();
|
||||
_nodeLimitResetTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
|
||||
import '../app_state.dart';
|
||||
import 'map_data_provider.dart';
|
||||
import 'network_status.dart';
|
||||
|
||||
/// Simple HTTP client that routes tile requests through the centralized MapDataProvider.
|
||||
/// This ensures all tile fetching (offline/online routing, retries, etc.) is in one place.
|
||||
class SimpleTileHttpClient extends http.BaseClient {
|
||||
final http.Client _inner = http.Client();
|
||||
final MapDataProvider _mapDataProvider = MapDataProvider();
|
||||
|
||||
// Tile completion tracking (brutalist approach)
|
||||
int _pendingTileRequests = 0;
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
// Extract tile coordinates from our custom URL scheme
|
||||
final tileCoords = _extractTileCoords(request.url);
|
||||
if (tileCoords != null) {
|
||||
final z = tileCoords['z']!;
|
||||
final x = tileCoords['x']!;
|
||||
final y = tileCoords['y']!;
|
||||
return _handleTileRequest(z, x, y);
|
||||
}
|
||||
|
||||
// Pass through non-tile requests
|
||||
return _inner.send(request);
|
||||
}
|
||||
|
||||
/// Extract z/x/y coordinates from our fake domain: https://tiles.local/provider/type/z/x/y
|
||||
/// We ignore the provider/type in the URL since we use current AppState for actual fetching
|
||||
Map<String, int>? _extractTileCoords(Uri url) {
|
||||
if (url.host != 'tiles.local') return null;
|
||||
|
||||
final pathSegments = url.pathSegments;
|
||||
if (pathSegments.length != 5) return null;
|
||||
|
||||
// pathSegments[0] = providerId (for cache separation only)
|
||||
// pathSegments[1] = tileTypeId (for cache separation only)
|
||||
final z = int.tryParse(pathSegments[2]);
|
||||
final x = int.tryParse(pathSegments[3]);
|
||||
final y = int.tryParse(pathSegments[4]);
|
||||
|
||||
if (z != null && x != null && y != null) {
|
||||
return {'z': z, 'x': x, 'y': y};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<http.StreamedResponse> _handleTileRequest(int z, int x, int y) async {
|
||||
// Increment pending counter (brutalist completion detection)
|
||||
_pendingTileRequests++;
|
||||
|
||||
try {
|
||||
// Always go through MapDataProvider - it handles offline/online routing
|
||||
// MapDataProvider will get current provider from AppState
|
||||
final tileBytes = await _mapDataProvider.getTile(z: z, x: x, y: y, source: MapSource.auto);
|
||||
|
||||
// Serve tile with proper cache headers
|
||||
return http.StreamedResponse(
|
||||
Stream.value(tileBytes),
|
||||
200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=604800',
|
||||
'Expires': _httpDateFormat(DateTime.now().add(Duration(days: 7))),
|
||||
'Last-Modified': _httpDateFormat(DateTime.now().subtract(Duration(hours: 1))),
|
||||
},
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('[SimpleTileService] Could not get tile $z/$x/$y: $e');
|
||||
|
||||
// Return 404 and let flutter_map handle it gracefully
|
||||
return http.StreamedResponse(
|
||||
Stream.value(<int>[]),
|
||||
404,
|
||||
reasonPhrase: 'Tile unavailable: $e',
|
||||
);
|
||||
} finally {
|
||||
// Decrement pending counter and report completion when all done
|
||||
_pendingTileRequests--;
|
||||
if (_pendingTileRequests == 0) {
|
||||
// Only report tile completion if we were in loading state (user-initiated)
|
||||
if (NetworkStatus.instance.currentStatus == NetworkStatusType.waiting) {
|
||||
NetworkStatus.instance.setSuccess();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear any queued tile requests when map view changes
|
||||
void clearTileQueue() {
|
||||
_mapDataProvider.clearTileQueue();
|
||||
}
|
||||
|
||||
/// Clear only tile requests that are no longer visible in the current bounds
|
||||
void clearStaleRequests(LatLngBounds currentBounds) {
|
||||
_mapDataProvider.clearTileQueueSelective(currentBounds);
|
||||
}
|
||||
|
||||
/// Format date for HTTP headers (RFC 7231)
|
||||
String _httpDateFormat(DateTime date) {
|
||||
final utc = date.toUtc();
|
||||
final weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
final months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
final weekday = weekdays[utc.weekday - 1];
|
||||
final day = utc.day.toString().padLeft(2, '0');
|
||||
final month = months[utc.month - 1];
|
||||
final year = utc.year;
|
||||
final hour = utc.hour.toString().padLeft(2, '0');
|
||||
final minute = utc.minute.toString().padLeft(2, '0');
|
||||
final second = utc.second.toString().padLeft(2, '0');
|
||||
|
||||
return '$weekday, $day $month $year $hour:$minute:$second GMT';
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
_inner.close();
|
||||
super.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user