// lib/widgets/shared_map_widgets.dart import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:get/get.dart'; import 'package:latlong2/latlong.dart'; class SharedMapWidget extends StatelessWidget { final MapController mapController; final List layers; final void Function(TapPosition, LatLng)? onLongPress; final void Function(TapPosition, LatLng)? onTap; final void Function(MapCamera, bool)? onPositionChanged; final MapControls controls; final LatLng initialCenter; final double initialZoom; final double minZoom; final double maxZoom; // Controller-owned state (csak megjelenítéshez) final RxBool? isFollowing; final RxBool? isNorthUp; final RxDouble? currentZoom; final RxDouble? currentRotationRad; // Controller-owned commands final VoidCallback? onZoomIn; final VoidCallback? onZoomOut; final VoidCallback? onCenterOnGps; final VoidCallback? onResetNorth; const SharedMapWidget({ super.key, required this.mapController, this.layers = const [], this.onLongPress, this.onTap, this.onPositionChanged, this.controls = const MapControls(), this.initialCenter = const LatLng(47.5, 19.0), this.initialZoom = 18.0, this.minZoom = 3.0, this.maxZoom = 25.0, this.isFollowing, this.isNorthUp, this.currentZoom, this.currentRotationRad, this.onZoomIn, this.onZoomOut, this.onCenterOnGps, this.onResetNorth, }); @override Widget build(BuildContext context) { return Stack( children: [ FlutterMap( mapController: mapController, options: MapOptions( initialCenter: initialCenter, initialZoom: initialZoom, minZoom: minZoom, maxZoom: maxZoom, onLongPress: onLongPress, onTap: onTap, onPositionChanged: onPositionChanged, interactionOptions: const InteractionOptions( flags: InteractiveFlag.all, ), ), children: [ TileLayer( urlTemplate: 'http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'], maxNativeZoom: 18, ), ...layers, ], ), _MapControlsOverlay( controls: controls, isFollowing: isFollowing, isNorthUp: isNorthUp, currentRotationRad: currentRotationRad, onZoomIn: onZoomIn, onZoomOut: onZoomOut, onCenterOnGps: onCenterOnGps, onResetNorth: onResetNorth, ), if (controls.showZoomLevel && currentZoom != null) Positioned( bottom: 150, left: 4, child: Obx(() => _ZoomLabel(currentZoom!.value)), ), ], ); } } class MapControls { final bool showZoomButtons; final bool showFollowButton; final bool showNorthButton; final bool showZoomLevel; final bool showCompass; const MapControls({ this.showZoomButtons = true, this.showFollowButton = true, this.showNorthButton = true, this.showZoomLevel = true, this.showCompass = false, }); const MapControls.fieldTrip() : showZoomButtons = true, showFollowButton = false, showNorthButton = true, showZoomLevel = true, showCompass = false; const MapControls.navigation() : showZoomButtons = true, showFollowButton = true, showNorthButton = true, showZoomLevel = false, showCompass = true; const MapControls.minimal() : showZoomButtons = true, showFollowButton = false, showNorthButton = false, showZoomLevel = false, showCompass = false; } class _MapControlsOverlay extends StatelessWidget { final MapControls controls; final RxBool? isFollowing; final RxBool? isNorthUp; final RxDouble? currentRotationRad; final VoidCallback? onZoomIn; final VoidCallback? onZoomOut; final VoidCallback? onCenterOnGps; final VoidCallback? onResetNorth; const _MapControlsOverlay({ required this.controls, required this.isFollowing, required this.isNorthUp, required this.currentRotationRad, required this.onZoomIn, required this.onZoomOut, required this.onCenterOnGps, required this.onResetNorth, }); @override Widget build(BuildContext context) { return Positioned( right: 10, bottom: 150, child: Column( mainAxisSize: MainAxisSize.min, children: [ if (controls.showNorthButton && isNorthUp != null) Obx(() { final active = isNorthUp!.value; final angle = currentRotationRad?.value ?? 0.0; return _ControlButton( tooltip: 'Észak felfelé', active: active, onTap: onResetNorth, child: Transform.rotate( angle: -angle, child: const Icon(Icons.navigation, size: 20), ), ); }), if (controls.showNorthButton) const SizedBox(height: 6), if (controls.showFollowButton && isFollowing != null) Obx(() => _ControlButton( tooltip: isFollowing!.value ? 'GPS követés aktív' : 'GPS követés kikapcsolva', active: isFollowing!.value, icon: isFollowing!.value ? Icons.gps_fixed : Icons.gps_not_fixed, onTap: onCenterOnGps, )), if (controls.showFollowButton) const SizedBox(height: 6), if (controls.showZoomButtons) ...[ _ControlButton( icon: Icons.add, tooltip: 'Nagyítás', onTap: onZoomIn, ), const SizedBox(height: 2), _ControlButton( icon: Icons.remove, tooltip: 'Kicsinyítés', onTap: onZoomOut, ), ], ], ), ); } } class _ControlButton extends StatelessWidget { final IconData? icon; final Widget? child; final String tooltip; final bool active; final VoidCallback? onTap; const _ControlButton({ this.icon, this.child, required this.tooltip, this.active = false, required this.onTap, }); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Tooltip( message: tooltip, child: Material( color: active ? Theme.of(context).colorScheme.primaryContainer : isDark ? Colors.grey.shade800.withOpacity(0.9) : Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(8), elevation: 3, shadowColor: Colors.black38, child: InkWell( borderRadius: BorderRadius.circular(8), onTap: onTap, child: SizedBox( width: 42, height: 42, child: Center( child: child ?? Icon( icon, size: 20, color: active ? Theme.of(context).colorScheme.primary : isDark ? Colors.white70 : Colors.black87, ), ), ), ), ), ); } } class _ZoomLabel extends StatelessWidget { final double zoom; const _ZoomLabel(this.zoom); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(6), ), child: Text( 'Z ${zoom.toStringAsFixed(1)}', style: const TextStyle(color: Colors.white70, fontSize: 11), ), ); } } class LabeledMarker extends StatelessWidget { final String label; final IconData icon; final Color color; final Color? activeColor; final String? sublabel; const LabeledMarker({ super.key, required this.label, required this.icon, required this.color, this.activeColor, this.sublabel, }); @override Widget build(BuildContext context) { final c = activeColor ?? color; return Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: c, borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( label, style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), if (sublabel != null) Text( sublabel!, style: TextStyle( color: Colors.white.withOpacity(0.9), fontSize: 9, ), ), ], ), ), Container(width: 2, height: 6, color: c), Icon( icon, color: c, size: 22, shadows: const [ Shadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)), ], ), ], ); } } class PulsingDot extends StatefulWidget { final Color color; const PulsingDot({super.key, required this.color}); @override State createState() => _PulsingDotState(); } class _PulsingDotState extends State with SingleTickerProviderStateMixin { late AnimationController _ctrl; late Animation _scale; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 1000), )..repeat(reverse: true); _scale = Tween(begin: 0.8, end: 1.2).animate( CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut), ); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) => ScaleTransition( scale: _scale, child: Container( width: 18, height: 18, decoration: BoxDecoration( color: widget.color, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ BoxShadow( color: widget.color.withOpacity(0.5), blurRadius: 8, spreadRadius: 2, ), ], ), ), ); }