MobilApp/lib/widgets/shared_map_widgets.dart

580 lines
18 KiB
Dart
Raw Normal View History

// 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<Widget> 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<SharedMapWidget> createState() => _SharedMapWidgetState();
}
class _SharedMapWidgetState extends State<SharedMapWidget> {
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<MapSurveyController>())
Obx(() => MarkerLayer(
markers: _buildMeasuredPointMarkers(),
)),
// 4. Kitűzési célpont + vonal
if (Get.isRegistered<MapSurveyController>())
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<Marker> _buildMeasuredPointMarkers() {
if (!Get.isRegistered<MapSurveyController>()) 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<MapSurveyController>())
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<double> _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)
],
),
),
);
}