MobilApp/lib/widgets/shared_map_widgets.dart

416 lines
11 KiB
Dart

// 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<Widget> 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<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,
),
],
),
),
);
}