mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-07-02 19:05:51 +02:00
holy crap tile types
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:latlong2/latlong.dart';
|
||||
|
||||
import 'models/camera_profile.dart';
|
||||
import 'models/pending_upload.dart';
|
||||
import 'models/tile_provider.dart';
|
||||
import 'services/offline_area_service.dart';
|
||||
import 'state/auth_state.dart';
|
||||
import 'state/profile_state.dart';
|
||||
@@ -13,6 +14,7 @@ import 'state/upload_queue_state.dart';
|
||||
// Re-export types for backward compatibility
|
||||
export 'state/settings_state.dart' show UploadMode;
|
||||
export 'state/session_state.dart' show AddCameraSession;
|
||||
export 'models/tile_provider.dart' show TileProviderType;
|
||||
|
||||
// ------------------ AppState ------------------
|
||||
class AppState extends ChangeNotifier {
|
||||
@@ -64,6 +66,7 @@ class AppState extends ChangeNotifier {
|
||||
bool get offlineMode => _settingsState.offlineMode;
|
||||
int get maxCameras => _settingsState.maxCameras;
|
||||
UploadMode get uploadMode => _settingsState.uploadMode;
|
||||
TileProviderType get tileProvider => _settingsState.tileProvider;
|
||||
|
||||
// Upload queue state
|
||||
int get pendingCount => _uploadQueueState.pendingCount;
|
||||
@@ -176,6 +179,10 @@ class AppState extends ChangeNotifier {
|
||||
_startUploader(); // Restart uploader with new mode
|
||||
}
|
||||
|
||||
Future<void> setTileProvider(TileProviderType provider) async {
|
||||
await _settingsState.setTileProvider(provider);
|
||||
}
|
||||
|
||||
// ---------- Queue Methods ----------
|
||||
void clearQueue() {
|
||||
_uploadQueueState.clearQueue();
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
enum TileProviderType {
|
||||
osmStreet,
|
||||
googleHybrid,
|
||||
arcgisSatellite,
|
||||
mapboxSatellite,
|
||||
}
|
||||
|
||||
class TileProviderConfig {
|
||||
final TileProviderType type;
|
||||
final String name;
|
||||
final String urlTemplate;
|
||||
final String attribution;
|
||||
final bool requiresApiKey;
|
||||
final String? description;
|
||||
|
||||
const TileProviderConfig({
|
||||
required this.type,
|
||||
required this.name,
|
||||
required this.urlTemplate,
|
||||
required this.attribution,
|
||||
this.requiresApiKey = false,
|
||||
this.description,
|
||||
});
|
||||
|
||||
/// Returns the URL template with API key inserted if needed
|
||||
String getUrlTemplate({String? apiKey}) {
|
||||
if (requiresApiKey && apiKey != null) {
|
||||
return urlTemplate.replaceAll('{api_key}', apiKey);
|
||||
}
|
||||
return urlTemplate;
|
||||
}
|
||||
|
||||
/// Check if this provider is available (has required API key if needed)
|
||||
bool isAvailable({String? apiKey}) {
|
||||
if (requiresApiKey) {
|
||||
return apiKey != null && apiKey.isNotEmpty;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Built-in tile provider configurations
|
||||
class TileProviders {
|
||||
static const osmStreet = TileProviderConfig(
|
||||
type: TileProviderType.osmStreet,
|
||||
name: 'Street Map',
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
description: 'Standard street map with roads, buildings, and labels',
|
||||
);
|
||||
|
||||
static const googleHybrid = TileProviderConfig(
|
||||
type: TileProviderType.googleHybrid,
|
||||
name: 'Satellite + Roads',
|
||||
urlTemplate: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}',
|
||||
attribution: '© Google',
|
||||
description: 'Satellite imagery with road and label overlays',
|
||||
);
|
||||
|
||||
static const arcgisSatellite = TileProviderConfig(
|
||||
type: TileProviderType.arcgisSatellite,
|
||||
name: 'Pure Satellite',
|
||||
urlTemplate: 'http://services.arcgisonline.com/ArcGis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png',
|
||||
attribution: '© Esri © Maxar',
|
||||
description: 'High-resolution satellite imagery without overlays',
|
||||
);
|
||||
|
||||
static const mapboxSatellite = TileProviderConfig(
|
||||
type: TileProviderType.mapboxSatellite,
|
||||
name: 'Pure Satellite (Mapbox)',
|
||||
urlTemplate: 'https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.jpg90?access_token={api_key}',
|
||||
attribution: '© Mapbox © Maxar',
|
||||
requiresApiKey: true,
|
||||
description: 'High-resolution satellite imagery without overlays',
|
||||
);
|
||||
|
||||
/// Get all available tile providers (those with API keys if required)
|
||||
static List<TileProviderConfig> getAvailable({String? mapboxApiKey}) {
|
||||
return [
|
||||
osmStreet,
|
||||
googleHybrid,
|
||||
arcgisSatellite,
|
||||
if (mapboxSatellite.isAvailable(apiKey: mapboxApiKey)) mapboxSatellite,
|
||||
];
|
||||
}
|
||||
|
||||
/// Get provider config by type
|
||||
static TileProviderConfig? getByType(TileProviderType type) {
|
||||
switch (type) {
|
||||
case TileProviderType.osmStreet:
|
||||
return osmStreet;
|
||||
case TileProviderType.googleHybrid:
|
||||
return googleHybrid;
|
||||
case TileProviderType.arcgisSatellite:
|
||||
return arcgisSatellite;
|
||||
case TileProviderType.mapboxSatellite:
|
||||
return mapboxSatellite;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'settings_screen_sections/offline_areas_section.dart';
|
||||
import 'settings_screen_sections/offline_mode_section.dart';
|
||||
import 'settings_screen_sections/about_section.dart';
|
||||
import 'settings_screen_sections/max_cameras_section.dart';
|
||||
import 'settings_screen_sections/tile_provider_section.dart';
|
||||
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
const SettingsScreen({super.key});
|
||||
@@ -28,6 +29,8 @@ class SettingsScreen extends StatelessWidget {
|
||||
Divider(),
|
||||
MaxCamerasSection(),
|
||||
Divider(),
|
||||
TileProviderSection(),
|
||||
Divider(),
|
||||
OfflineModeSection(),
|
||||
Divider(),
|
||||
OfflineAreasSection(),
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../app_state.dart';
|
||||
import '../../models/tile_provider.dart';
|
||||
|
||||
class TileProviderSection extends StatelessWidget {
|
||||
const TileProviderSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appState = context.watch<AppState>();
|
||||
final currentProvider = appState.tileProvider;
|
||||
|
||||
// Get available providers (for now, all free ones are available)
|
||||
final availableProviders = [
|
||||
TileProviders.osmStreet,
|
||||
TileProviders.googleHybrid,
|
||||
TileProviders.arcgisSatellite,
|
||||
// Don't include Mapbox for now since we don't have API key handling
|
||||
];
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Map Type',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...availableProviders.map((config) {
|
||||
final isSelected = config.type == currentProvider;
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Radio<TileProviderType>(
|
||||
value: config.type,
|
||||
groupValue: currentProvider,
|
||||
onChanged: (TileProviderType? value) {
|
||||
if (value != null) {
|
||||
appState.setTileProvider(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
title: Text(config.name),
|
||||
subtitle: config.description != null
|
||||
? Text(
|
||||
config.description!,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
appState.setTileProvider(config.type);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,12 @@ class SimpleTileHttpClient extends http.BaseClient {
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
// Only intercept tile requests to OSM
|
||||
// Only intercept tile requests to OSM (for now - other providers pass through)
|
||||
if (request.url.host == 'tile.openstreetmap.org') {
|
||||
return _handleTileRequest(request);
|
||||
}
|
||||
|
||||
// Pass through all other requests
|
||||
// Pass through all other requests (Google, Mapbox, etc.)
|
||||
return _inner.send(request);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
||||
import '../models/tile_provider.dart';
|
||||
|
||||
// Enum for upload mode (Production, OSM Sandbox, Simulate)
|
||||
enum UploadMode { production, sandbox, simulate }
|
||||
@@ -10,16 +10,19 @@ class SettingsState extends ChangeNotifier {
|
||||
static const String _offlineModePrefsKey = 'offline_mode';
|
||||
static const String _maxCamerasPrefsKey = 'max_cameras';
|
||||
static const String _uploadModePrefsKey = 'upload_mode';
|
||||
static const String _tileProviderPrefsKey = 'tile_provider';
|
||||
static const String _legacyTestModePrefsKey = 'test_mode';
|
||||
|
||||
bool _offlineMode = false;
|
||||
int _maxCameras = 250;
|
||||
UploadMode _uploadMode = UploadMode.simulate;
|
||||
TileProviderType _tileProvider = TileProviderType.osmStreet;
|
||||
|
||||
// Getters
|
||||
bool get offlineMode => _offlineMode;
|
||||
int get maxCameras => _maxCameras;
|
||||
UploadMode get uploadMode => _uploadMode;
|
||||
TileProviderType get tileProvider => _tileProvider;
|
||||
|
||||
// Initialize settings from preferences
|
||||
Future<void> init() async {
|
||||
@@ -46,6 +49,14 @@ class SettingsState extends ChangeNotifier {
|
||||
await prefs.remove(_legacyTestModePrefsKey);
|
||||
await prefs.setInt(_uploadModePrefsKey, _uploadMode.index);
|
||||
}
|
||||
|
||||
// Load tile provider
|
||||
if (prefs.containsKey(_tileProviderPrefsKey)) {
|
||||
final idx = prefs.getInt(_tileProviderPrefsKey) ?? 0;
|
||||
if (idx >= 0 && idx < TileProviderType.values.length) {
|
||||
_tileProvider = TileProviderType.values[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setOfflineMode(bool enabled) async {
|
||||
@@ -70,4 +81,11 @@ class SettingsState extends ChangeNotifier {
|
||||
await prefs.setInt(_uploadModePrefsKey, mode.index);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setTileProvider(TileProviderType provider) async {
|
||||
_tileProvider = provider;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_tileProviderPrefsKey, provider.index);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import '../services/simple_tile_service.dart';
|
||||
import '../services/network_status.dart';
|
||||
import '../models/osm_camera_node.dart';
|
||||
import '../models/camera_profile.dart';
|
||||
import '../models/tile_provider.dart';
|
||||
import 'debouncer.dart';
|
||||
import 'camera_provider_with_cache.dart';
|
||||
import 'map/camera_markers.dart';
|
||||
@@ -169,6 +170,41 @@ class MapViewState extends State<MapView> {
|
||||
return ids1.length == ids2.length && ids1.containsAll(ids2);
|
||||
}
|
||||
|
||||
/// Build tile layer based on selected tile provider
|
||||
Widget _buildTileLayer(AppState appState) {
|
||||
final providerConfig = TileProviders.getByType(appState.tileProvider);
|
||||
if (providerConfig == null) {
|
||||
// Fallback to OSM if somehow we have an invalid provider
|
||||
return TileLayer(
|
||||
urlTemplate: TileProviders.osmStreet.urlTemplate,
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// For OSM tiles, use our custom HTTP client for offline/online routing
|
||||
if (providerConfig.type == TileProviderType.osmStreet) {
|
||||
return TileLayer(
|
||||
urlTemplate: providerConfig.urlTemplate,
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// For other providers, use standard HTTP client (no offline support yet)
|
||||
return TileLayer(
|
||||
urlTemplate: providerConfig.urlTemplate,
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
additionalOptions: {
|
||||
'attribution': providerConfig.attribution,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
@@ -238,7 +274,7 @@ class MapViewState extends State<MapView> {
|
||||
return Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
key: ValueKey('map_offline_${appState.offlineMode}'),
|
||||
key: ValueKey('map_offline_${appState.offlineMode}_provider_${appState.tileProvider.name}'),
|
||||
mapController: _controller,
|
||||
options: MapOptions(
|
||||
initialCenter: _currentLatLng ?? LatLng(37.7749, -122.4194),
|
||||
@@ -273,13 +309,7 @@ class MapViewState extends State<MapView> {
|
||||
},
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
),
|
||||
),
|
||||
_buildTileLayer(appState),
|
||||
cameraLayers,
|
||||
// Built-in scale bar from flutter_map
|
||||
Scalebar(
|
||||
|
||||
Reference in New Issue
Block a user