feat(map): migrate to flutter_map 8.2 with OSM-compliant tiles

This commit is contained in:
2026-02-17 16:41:02 +01:00
parent f709d72f50
commit 4b9b9a2327
5 changed files with 88 additions and 143 deletions

View File

@ -34,12 +34,14 @@ Future<void> main() async {
create: (BuildContext context) => GetIt.I.get<RefreshCubit>(), create: (BuildContext context) => GetIt.I.get<RefreshCubit>(),
), ),
], ],
child: MyApp(), child: const MyApp(),
), ),
); );
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(

View File

@ -1,14 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_cache/flutter_map_cache.dart';
import 'package:flutter_map_compass/flutter_map_compass.dart'; import 'package:flutter_map_compass/flutter_map_compass.dart';
import 'package:flutter_map_math/flutter_geo_math.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@ -18,15 +11,9 @@ import 'package:ot_viewer_app/settings_page.dart';
import 'package:ot_viewer_app/user_path_bloc.dart'; import 'package:ot_viewer_app/user_path_bloc.dart';
import 'package:ot_viewer_app/util.dart'; import 'package:ot_viewer_app/util.dart';
import 'package:ot_viewer_app/web_socket_cubit.dart'; import 'package:ot_viewer_app/web_socket_cubit.dart';
import 'package:path_provider/path_provider.dart';
import 'owntracks_api.dart'; import 'owntracks_api.dart';
import 'dart:math' as math; import 'dart:math' as math;
Future<String> getPath() async {
final cacheDirectory = await getTemporaryDirectory();
return cacheDirectory.path;
}
class MapPage extends StatefulWidget { class MapPage extends StatefulWidget {
const MapPage({super.key}); const MapPage({super.key});
@ -55,14 +42,8 @@ class _MapPageState extends State<MapPage> {
refreshCubit.stream.listen((_) => cubit.reconnect()); refreshCubit.stream.listen((_) => cubit.reconnect());
return cubit; return cubit;
}, },
child: FutureBuilder<String>( child: BlocBuilder<SettingsCubit, SettingsState>(
future: getPath(),
builder: (something, tempPath) =>
BlocBuilder<SettingsCubit, SettingsState>(
builder: (context, state) { builder: (context, state) {
if (tempPath.data == null) {
return const Center(child: Text('Loading Map...'));
}
return Stack(children: [ return Stack(children: [
FlutterMap( FlutterMap(
mapController: _mapController, mapController: _mapController,
@ -72,12 +53,13 @@ class _MapPageState extends State<MapPage> {
), ),
children: [ children: [
TileLayer( TileLayer(
urlTemplate: urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'ot_viewer_app',
tileProvider: CachedTileProvider( tileProvider: NetworkTileProvider(
maxStale: const Duration(days: 30), headers: {
store: HiveCacheStore(tempPath.data!, 'User-Agent': 'ot_viewer_app/1.0',
hiveBoxName: 'HiveCacheStore')), },
),
), ),
...state.activeDevices.map((id) => ...state.activeDevices.map((id) =>
UserPath(key: ValueKey(id), device: id, settings: state)), UserPath(key: ValueKey(id), device: id, settings: state)),
@ -105,7 +87,6 @@ class _MapPageState extends State<MapPage> {
]); ]);
}, },
), ),
),
); );
} }
} }
@ -140,7 +121,6 @@ class _UserPathState extends State<UserPath> {
if (state if (state
case LocationUpdateReceived( case LocationUpdateReceived(
:final position, :final position,
:final deviceId
)) { )) {
if (userPathBloc.deviceId == state.deviceId) { if (userPathBloc.deviceId == state.deviceId) {
userPathBloc.add(UserPathLiveSubscriptionUpdate(position)); userPathBloc.add(UserPathLiveSubscriptionUpdate(position));
@ -161,15 +141,15 @@ class _UserPathState extends State<UserPath> {
.add(UserPathLoginDataChanged(widget.settings)); .add(UserPathLoginDataChanged(widget.settings));
print("rebuild"); print("rebuild");
final _istate = state as MainUserPathState; final mainState = state as MainUserPathState;
// make markers // make markers
final List<Marker> markers = []; final List<Marker> markers = [];
if (state.livePoints.isNotEmpty) { if (mainState.livePoints.isNotEmpty) {
markers.add(Marker( markers.add(Marker(
width: 500, width: 500,
height: 100, height: 100,
point: state.livePoints.last.asLatLng, point: mainState.livePoints.last.asLatLng,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -205,10 +185,11 @@ class _UserPathState extends State<UserPath> {
// Create fancy fade-out and blended line (TODO: make distance based. use flutter_map_math) // Create fancy fade-out and blended line (TODO: make distance based. use flutter_map_math)
List<List<LatLng>> segments = []; List<List<LatLng>> segments = [];
List<Color> colors = []; List<Color> colors = [];
if (state.initialPoints.isNotEmpty) { if (mainState.initialPoints.isNotEmpty) {
final allPoints = final allPoints = mainState.initialPoints
state.initialPoints.map((e) => LatLng(e.lat, e.lon)).toList(); .map((e) => LatLng(e.lat, e.lon))
final segmentCount = math.min(100, allPoints.length); .toList();
final int segmentCount = math.min(100, allPoints.length);
final pointsPerSegment = (allPoints.length / segmentCount).ceil(); final pointsPerSegment = (allPoints.length / segmentCount).ceil();
// Split the points into segments and generate colors // Split the points into segments and generate colors
@ -259,7 +240,7 @@ class _UserPathState extends State<UserPath> {
key: ValueKey('${widget.device}_liveLines'), key: ValueKey('${widget.device}_liveLines'),
polylines: [ polylines: [
Polyline( Polyline(
points: state.livePoints points: mainState.livePoints
.map((e) => LatLng(e.lat, e.lon)) .map((e) => LatLng(e.lat, e.lon))
.toList(), .toList(),
strokeWidth: 4.0, strokeWidth: 4.0,

View File

@ -113,38 +113,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
dio: dart_polylabel2:
dependency: transitive dependency: transitive
description: description:
name: dio name: dart_polylabel2
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.8.0+1" version: "1.0.0"
dio_cache_interceptor:
dependency: transitive
description:
name: dio_cache_interceptor
sha256: "1346705a2057c265014d7696e3e2318b560bfb00b484dac7f9b01e2ceaebb07d"
url: "https://pub.dev"
source: hosted
version: "3.5.1"
dio_cache_interceptor_hive_store:
dependency: "direct main"
description:
name: dio_cache_interceptor_hive_store
sha256: "449b36541216cb20543228081125ad2995eb9712ec35bd030d85663ea1761895"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
duration_picker: duration_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -234,18 +210,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_map name: flutter_map
sha256: "2ecb34619a4be19df6f40c2f8dce1591675b4eff7a6857bd8f533706977385da" sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.2" version: "8.2.2"
flutter_map_cache:
dependency: "direct main"
description:
name: flutter_map_cache
sha256: "5b30c9b0d36315a22f4ee070737104a6017e7ff990e8addc8128ba81786e03ef"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
flutter_map_compass: flutter_map_compass:
dependency: "direct main" dependency: "direct main"
description: description:
@ -292,10 +260,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: http name: http
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.6.0"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -512,14 +480,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
polylabel:
dependency: transitive
description:
name: polylabel
sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
posix: posix:
dependency: transitive dependency: transitive
description: description:

View File

@ -35,7 +35,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6 cupertino_icons: ^1.0.6
flutter_map: ^7.0.2 flutter_map: ^8.2.0
http: ^1.2.1 http: ^1.2.1
flutter_dotenv: ^5.1.0 flutter_dotenv: ^5.1.0
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
@ -50,8 +50,6 @@ dependencies:
fast_immutable_collections: ^10.1.2 fast_immutable_collections: ^10.1.2
flutter_map_compass: ^1.0.1 flutter_map_compass: ^1.0.1
# flutter_map_location_marker: ^8.0.8 # flutter_map_location_marker: ^8.0.8
flutter_map_cache: ^1.5.0
dio_cache_interceptor_hive_store: ^3.2.2
flutter_map_math: ^0.1.7 flutter_map_math: ^0.1.7
intl: ^0.19.0 intl: ^0.19.0
get_it: ^7.6.7 get_it: ^7.6.7

View File

@ -1,30 +1,34 @@
// This is a basic Flutter widget test. import 'dart:io';
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:ot_viewer_app/main.dart'; import 'package:ot_viewer_app/main.dart';
import 'package:ot_viewer_app/refresh_cubit.dart';
import 'package:ot_viewer_app/settings_page.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { setUpAll(() async {
// Build our app and trigger a frame. TestWidgetsFlutterBinding.ensureInitialized();
await tester.pumpWidget(const MyApp()); final tempDir = await Directory.systemTemp.createTemp('ot_viewer_test');
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: tempDir,
);
});
// Verify that our counter starts at 0. testWidgets('App shell renders with map and settings tabs',
expect(find.text('0'), findsOneWidget); (WidgetTester tester) async {
expect(find.text('1'), findsNothing); await tester.pumpWidget(
MultiBlocProvider(
providers: [
BlocProvider(create: (context) => SettingsCubit()),
BlocProvider(create: (context) => RefreshCubit()),
],
child: const MyApp(),
),
);
// Tap the '+' icon and trigger a frame. expect(find.text('Map'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add)); expect(find.text('Settings'), findsOneWidget);
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
}); });
} }