import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart' as ll; import 'map_provider_interface.dart'; class OpenStreetMapProvider implements AppMapProvider { @override AppMapType get type => AppMapType.openStreetMap; @override String get name => 'OpenStreetMap'; @override Widget buildMapView({ required BuildContext context, required AppLatLng center, required double zoom, double bearing = 0.0, required List markers, required List polylines, required bool isFollowMode, required MapConfig config, required ValueChanged? onPositionChanged, required VoidCallback? onUserInteraction, }) { return _OsmMapView( center: center, zoom: zoom, bearing: bearing, markers: markers, polylines: polylines, isFollowMode: isFollowMode, config: config, onPositionChanged: onPositionChanged, onUserInteraction: onUserInteraction, ); } } class _OsmMapView extends StatefulWidget { final AppLatLng center; final double zoom; final double bearing; final List markers; final List polylines; final bool isFollowMode; final MapConfig config; final ValueChanged? onPositionChanged; final VoidCallback? onUserInteraction; const _OsmMapView({ required this.center, required this.zoom, this.bearing = 0.0, required this.markers, required this.polylines, required this.isFollowMode, required this.config, required this.onPositionChanged, required this.onUserInteraction, }); @override State<_OsmMapView> createState() => _OsmMapViewState(); } class _OsmMapViewState extends State<_OsmMapView> { late final MapController _mapController; ll.LatLng? _lastFollowPos; DateTime? _lastFollowTime; @override void initState() { super.initState(); _mapController = MapController(); } @override void didUpdateWidget(covariant _OsmMapView oldWidget) { super.didUpdateWidget(oldWidget); final centerLl = ll.LatLng(widget.center.latitude, widget.center.longitude); final zoomChanged = (oldWidget.zoom - widget.zoom).abs() > 0.01; final bearingChanged = (oldWidget.bearing - widget.bearing).abs() > 0.1; final justBecameFollowMode = !oldWidget.isFollowMode && widget.isFollowMode; if (widget.isFollowMode) { final now = DateTime.now(); final posDelta = _lastFollowPos == null ? 1.0 : (centerLl.latitude - _lastFollowPos!.latitude).abs() + (centerLl.longitude - _lastFollowPos!.longitude).abs(); final shouldMove = justBecameFollowMode || _lastFollowPos == null || zoomChanged || (posDelta > 0.00005 && (_lastFollowTime == null || now.difference(_lastFollowTime!).inMilliseconds > 500)); if (shouldMove) { _lastFollowPos = centerLl; _lastFollowTime = now; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { try { _mapController.move(centerLl, widget.zoom); } catch (_) {} } }); } } else if (zoomChanged) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { try { _mapController.move(_mapController.camera.center, widget.zoom); } catch (_) {} } }); } if (bearingChanged) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { try { _mapController.rotate(widget.bearing); } catch (_) {} } }); } } @override void dispose() { _mapController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final centerLl = ll.LatLng(widget.center.latitude, widget.center.longitude); final media = MediaQuery.of(context); final sizeKey = '${media.orientation}_${media.size.width.toInt()}x${media.size.height.toInt()}'; return FlutterMap( key: ValueKey('osm_map_$sizeKey'), mapController: _mapController, options: MapOptions( initialCenter: centerLl, initialZoom: widget.zoom, initialRotation: widget.bearing, minZoom: widget.config.minZoom, maxZoom: widget.config.maxZoom, backgroundColor: widget.config.isDarkMode ? const Color(0xFF0F0F0F) : const Color(0xFFE5E5E5), onPositionChanged: (position, hasGesture) { if (hasGesture) { widget.onUserInteraction?.call(); } if (widget.onPositionChanged != null) { final c = position.center; widget.onPositionChanged!(AppLatLng(c.latitude, c.longitude)); } }, ), children: [ TileLayer( key: ValueKey('${widget.config.tileUrlTemplate}_${widget.config.isDarkMode}'), urlTemplate: widget.config.tileUrlTemplate, subdomains: widget.config.subdomains, userAgentPackageName: 'app.motorsignal.motorcycle', tileBuilder: widget.config.isDarkMode ? darkModeTileBuilder : null, maxZoom: 20, maxNativeZoom: 19, ), PolylineLayer( polylines: widget.polylines.map((p) { return Polyline( points: p.points.map((pt) => ll.LatLng(pt.latitude, pt.longitude)).toList(), strokeWidth: p.width, color: p.color, ); }).toList(), ), MarkerLayer( markers: widget.markers.map((m) { return Marker( point: ll.LatLng(m.position.latitude, m.position.longitude), width: 50, height: 50, child: GestureDetector( onTap: m.onTap, child: Transform.rotate( angle: (m.heading * math.pi) / 180.0, child: m.iconWidget ?? const Icon( Icons.navigation, color: Colors.cyanAccent, size: 36, ), ), ), ); }).toList(), ), RichAttributionWidget( attributions: [ TextSourceAttribution( widget.config.attributionText, textStyle: const TextStyle(color: Colors.white70, fontSize: 10), ), ], ), ], ); } }