// lib/widgets/shared_map_widget.dart import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:get/get.dart'; import 'package:latlong2/latlong.dart'; import '../services/gnss/gnss_service.dart'; import '../services/coord_converter_service.dart'; import '../pages/map_survey/presentations/controllers/map_survey_controller.dart'; // ─── SharedMapWidget ────────────────────────────────────────────────────────── class SharedMapWidget extends StatefulWidget { /// Extra flutter_map rétegek (pl. PolylineLayer, PolygonLayer). final List extraLayers; /// Külső MapController — ha az oldal saját maga is mozgatja a térképet. final MapController? mapController; /// Hosszú nyomás callback (terepbejárás pont hozzáadáshoz). final void Function(TapPosition, LatLng)? onLongPress; /// Megjelenítendő vezérlők. final MapControls controls; /// Kezdeti zoom szint. final double initialZoom; const SharedMapWidget({ super.key, this.extraLayers = const [], this.mapController, this.onLongPress, this.controls = const MapControls(), this.initialZoom = 18.0, }); @override State createState() => _SharedMapWidgetState(); } class _SharedMapWidgetState extends State { late final MapController _mapController; // Reaktív belső állapot final _isFollowing = true.obs; // GPS követés be/ki final _currentZoom = 18.0.obs; final _isNorthUp = true.obs; // forgó térkép vs. É-up @override void initState() { super.initState(); _mapController = widget.mapController ?? MapController(); _currentZoom.value = widget.initialZoom; } @override void dispose() { // Csak akkor disposoljuk, ha belső controller if (widget.mapController == null) { _mapController.dispose(); } super.dispose(); } // ── GPS pozíció követése ──────────────────────────────────────────── void _onPositionChanged(MapCamera camera, bool hasGesture) { _currentZoom.value = camera.zoom; // Ha a felhasználó manuálisan mozgatja → kikapcsol a követés if (hasGesture && _isFollowing.value) { _isFollowing.value = false; } } void _centerOnPosition() { final gnss = GnssService.to; if (gnss.latitude.value == 0) return; _mapController.move( LatLng(gnss.latitude.value, gnss.longitude.value), _currentZoom.value, ); _isFollowing.value = true; } void _zoomIn() => _mapController.move(_mapController.camera.center, _currentZoom.value + 1); void _zoomOut() => _mapController.move(_mapController.camera.center, _currentZoom.value - 1); void _resetNorth() { _mapController.rotate(0); _isNorthUp.value = true; } // ── GPS stream → térkép mozgatás ─────────────────────────────────── @override Widget build(BuildContext context) { return Obx(() { final gnss = GnssService.to; final lat = gnss.latitude.value; final lon = gnss.longitude.value; if (_isFollowing.value && lat != 0 && lon != 0) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; _mapController.move(LatLng(lat, lon), _currentZoom.value); }); } return Stack( children: [ // ── Térkép ──────────────────────────────────────────────── FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: lat != 0 ? LatLng(lat, lon) : const LatLng(47.5, 19.0), initialZoom: widget.initialZoom, maxZoom: 25, minZoom: 3, onLongPress: widget.onLongPress, onPositionChanged: _onPositionChanged, interactionOptions: const InteractionOptions( flags: InteractiveFlag.all, ), ), children: [ // 1. Alaptérkép TileLayer( urlTemplate: 'http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'], maxNativeZoom: 18, ), // 2. Extra rétegek (terepbejárás elemei) ...widget.extraLayers, // 3. Bemért pontok if (Get.isRegistered()) Obx(() => MarkerLayer( markers: _buildMeasuredPointMarkers(), )), // 4. Kitűzési célpont + vonal if (Get.isRegistered()) Obx(() => _buildStakeoutLayer(lat, lon)), // 5. GPS pozíció Obx(() { final lat = GnssService.to.latitude.value; final lon = GnssService.to.longitude.value; return MarkerLayer( markers: lat == 0 && lon == 0 ? [] : [_buildCurrentPositionMarker(lat, lon)], ); }), ], ), // ── Vezérlők ────────────────────────────────────────────── _MapControlsOverlay( controls: widget.controls, isFollowing: _isFollowing, isNorthUp: _isNorthUp, currentZoom: _currentZoom, onZoomIn: _zoomIn, onZoomOut: _zoomOut, onCenterOnGps: _centerOnPosition, onResetNorth: _resetNorth, ), // ── Zoom szint jelzés (opcionális) ──────────────────────── if (widget.controls.showZoomLevel) Positioned( bottom: 8, left: 8, child: Obx(() => _ZoomLabel(_currentZoom.value)), ), ], ); }); } // ── Marker builder metódusok ──────────────────────────────────────── List _buildMeasuredPointMarkers() { if (!Get.isRegistered()) return []; return MapSurveyController.to.measuredPoints1 .map((point) => Marker( point: LatLng(point.latitude, point.longitude), width: 100, height: 56, alignment: Alignment.bottomCenter, child: _LabeledMarker( label: point.name, icon: Icons.location_on, color: Colors.blue, ), )) .toList(); } Widget _buildStakeoutLayer(double lat, double lon) { if (!Get.isRegistered()) return const SizedBox.shrink(); final ctrl = MapSurveyController.to; if (ctrl.mode.value != MapSurveyMode.stakeout || ctrl.targetEovY.value == 0) { return const SizedBox.shrink(); } final wgs = CoordConverterService.to .eovToWgsPoint(ctrl.targetEovY.value, ctrl.targetEovX.value); final targetLatLng = LatLng(wgs.y, wgs.x); return Stack(children: [ // Szaggatott vonal PolylineLayer(polylines: [ Polyline( points: [LatLng(lat, lon), targetLatLng], color: Colors.orange.withOpacity(0.85), strokeWidth: 2.5, ), ]), // Célpont marker MarkerLayer(markers: [ Marker( point: targetLatLng, width: 130, height: 72, alignment: Alignment.bottomCenter, child: _LabeledMarker( label: ctrl.targetName.value, icon: Icons.flag, color: Colors.orange, activeColor: ctrl.isOnTarget ? Colors.green : null, sublabel: ctrl.isOnTarget ? '✓ Célponton' : '${ctrl.distanceToTarget.toStringAsFixed(3)} m', ), ), ]), ]); } Marker _buildCurrentPositionMarker(double lat, double lon) { final color = switch (GnssService.to.gpsQuality.value) { 4 => Colors.green, 5 => Colors.lightGreen, 2 => Colors.blue, 1 => Colors.orange, _ => Colors.grey, }; return Marker( point: LatLng(lat, lon), width: 24, height: 24, child: _PulsingDot(color: color), ); } } // ─── Vezérlők konfigurációja ────────────────────────────────────────────────── 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, }); /// Terepbejárás módhoz — nincs follow (rajzolás közben szabad mozgás) const MapControls.fieldTrip() : showZoomButtons = true, showFollowButton = false, showNorthButton = true, showZoomLevel = true, showCompass = false; /// Navigáció módhoz — minden vezérlő const MapControls.navigation() : showZoomButtons = true, showFollowButton = true, showNorthButton = true, showZoomLevel = false, showCompass = true; /// Minimális — csak zoom const MapControls.minimal() : showZoomButtons = true, showFollowButton = false, showNorthButton = false, showZoomLevel = false, showCompass = false; } // ─── Vezérlők overlay ──────────────────────────────────────────────────────── class _MapControlsOverlay extends StatelessWidget { final MapControls controls; final RxBool isFollowing; final RxBool isNorthUp; final RxDouble currentZoom; 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.currentZoom, required this.onZoomIn, required this.onZoomOut, required this.onCenterOnGps, required this.onResetNorth, }); @override Widget build(BuildContext context) { return Positioned( right: 10, bottom: 80, // BottomNav felett child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── Iránytű / É-ra forgat ───────────────────────────── if (controls.showNorthButton) Obx(() => _ControlButton( icon: Icons.navigation, tooltip: 'Észak felfelé', active: isNorthUp.value, // A gomb elfordul ahogy a térkép forog — vizuális jelzés child: Transform.rotate( angle: 0, child: const Icon(Icons.navigation, size: 20), ), onTap: onResetNorth, )), if (controls.showNorthButton) const SizedBox(height: 6), // ── GPS követés ─────────────────────────────────────── if (controls.showFollowButton) 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), // ── Zoom gombok ─────────────────────────────────────── 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, ), ), ), ), ), ); } } // ─── Zoom szint label ───────────────────────────────────────────────────────── 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), ), ); } } // ─── Feliratozott marker ────────────────────────────────────────────────────── class _LabeledMarker extends StatelessWidget { final String label; final IconData icon; final Color color; final Color? activeColor; final String? sublabel; const _LabeledMarker({ 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)) ]), ], ); } } // ─── Pulzáló GPS pont ───────────────────────────────────────────────────────── class _PulsingDot extends StatefulWidget { final Color color; const _PulsingDot({required this.color}); @override State<_PulsingDot> createState() => _PulsingDotState(); } class _PulsingDotState extends State<_PulsingDot> 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) ], ), ), ); }