holy crap tile types

This commit is contained in:
stopflock
2025-08-23 21:40:36 -05:00
parent 7bd6f68a99
commit e65b9f58a6
7 changed files with 229 additions and 11 deletions
+7
View File
@@ -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();
+100
View File
@@ -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;
}
}
}
+3
View File
@@ -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);
},
);
}),
],
);
}
}
+2 -2
View File
@@ -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);
}
+19 -1
View File
@@ -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();
}
}
+38 -8
View File
@@ -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(