2026-05-24 14:50:31 +02:00
|
|
|
// lib/widgets/shared_map_widgets.dart
|
2026-05-12 00:14:11 +02:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
|
|
|
import 'package:get/get.dart';
|
|
|
|
|
import 'package:latlong2/latlong.dart';
|
|
|
|
|
|
2026-05-24 14:50:31 +02:00
|
|
|
class SharedMapWidget extends StatelessWidget {
|
|
|
|
|
final MapController mapController;
|
|
|
|
|
final List<Widget> layers;
|
2026-05-12 00:14:11 +02:00
|
|
|
final void Function(TapPosition, LatLng)? onLongPress;
|
2026-06-19 12:53:50 +02:00
|
|
|
final void Function(TapPosition, LatLng)? onTap;
|
2026-05-24 14:50:31 +02:00
|
|
|
final void Function(MapCamera, bool)? onPositionChanged;
|
2026-05-12 00:14:11 +02:00
|
|
|
|
|
|
|
|
final MapControls controls;
|
2026-05-24 14:50:31 +02:00
|
|
|
final LatLng initialCenter;
|
2026-05-12 00:14:11 +02:00
|
|
|
final double initialZoom;
|
2026-05-24 14:50:31 +02:00
|
|
|
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;
|
2026-05-12 00:14:11 +02:00
|
|
|
|
|
|
|
|
const SharedMapWidget({
|
|
|
|
|
super.key,
|
2026-05-24 14:50:31 +02:00
|
|
|
required this.mapController,
|
|
|
|
|
this.layers = const [],
|
2026-05-12 00:14:11 +02:00
|
|
|
this.onLongPress,
|
2026-06-19 12:53:50 +02:00
|
|
|
this.onTap,
|
2026-05-24 14:50:31 +02:00
|
|
|
this.onPositionChanged,
|
2026-05-12 00:14:11 +02:00
|
|
|
this.controls = const MapControls(),
|
2026-05-24 14:50:31 +02:00
|
|
|
this.initialCenter = const LatLng(47.5, 19.0),
|
2026-05-12 00:14:11 +02:00
|
|
|
this.initialZoom = 18.0,
|
2026-05-24 14:50:31 +02:00
|
|
|
this.minZoom = 3.0,
|
|
|
|
|
this.maxZoom = 25.0,
|
|
|
|
|
this.isFollowing,
|
|
|
|
|
this.isNorthUp,
|
|
|
|
|
this.currentZoom,
|
|
|
|
|
this.currentRotationRad,
|
|
|
|
|
this.onZoomIn,
|
|
|
|
|
this.onZoomOut,
|
|
|
|
|
this.onCenterOnGps,
|
|
|
|
|
this.onResetNorth,
|
2026-05-12 00:14:11 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-05-24 14:50:31 +02:00
|
|
|
return Stack(
|
|
|
|
|
children: [
|
|
|
|
|
FlutterMap(
|
|
|
|
|
mapController: mapController,
|
|
|
|
|
options: MapOptions(
|
|
|
|
|
initialCenter: initialCenter,
|
|
|
|
|
initialZoom: initialZoom,
|
|
|
|
|
minZoom: minZoom,
|
|
|
|
|
maxZoom: maxZoom,
|
|
|
|
|
onLongPress: onLongPress,
|
2026-06-19 12:53:50 +02:00
|
|
|
onTap: onTap,
|
2026-05-24 14:50:31 +02:00
|
|
|
onPositionChanged: onPositionChanged,
|
|
|
|
|
interactionOptions: const InteractionOptions(
|
|
|
|
|
flags: InteractiveFlag.all,
|
2026-05-12 00:14:11 +02:00
|
|
|
),
|
|
|
|
|
),
|
2026-05-24 14:50:31 +02:00
|
|
|
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,
|
2026-05-12 00:14:11 +02:00
|
|
|
),
|
2026-05-24 14:50:31 +02:00
|
|
|
...layers,
|
|
|
|
|
],
|
2026-05-12 00:14:11 +02:00
|
|
|
),
|
2026-05-24 14:50:31 +02:00
|
|
|
_MapControlsOverlay(
|
|
|
|
|
controls: controls,
|
|
|
|
|
isFollowing: isFollowing,
|
|
|
|
|
isNorthUp: isNorthUp,
|
|
|
|
|
currentRotationRad: currentRotationRad,
|
|
|
|
|
onZoomIn: onZoomIn,
|
|
|
|
|
onZoomOut: onZoomOut,
|
|
|
|
|
onCenterOnGps: onCenterOnGps,
|
|
|
|
|
onResetNorth: onResetNorth,
|
2026-05-12 00:14:11 +02:00
|
|
|
),
|
2026-05-24 14:50:31 +02:00
|
|
|
if (controls.showZoomLevel && currentZoom != null)
|
|
|
|
|
Positioned(
|
|
|
|
|
bottom: 8,
|
|
|
|
|
left: 8,
|
|
|
|
|
child: Obx(() => _ZoomLabel(currentZoom!.value)),
|
|
|
|
|
),
|
|
|
|
|
],
|
2026-05-12 00:14:11 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-24 14:50:31 +02:00
|
|
|
final RxBool? isFollowing;
|
|
|
|
|
final RxBool? isNorthUp;
|
|
|
|
|
final RxDouble? currentRotationRad;
|
|
|
|
|
|
|
|
|
|
final VoidCallback? onZoomIn;
|
|
|
|
|
final VoidCallback? onZoomOut;
|
|
|
|
|
final VoidCallback? onCenterOnGps;
|
|
|
|
|
final VoidCallback? onResetNorth;
|
2026-05-12 00:14:11 +02:00
|
|
|
|
|
|
|
|
const _MapControlsOverlay({
|
|
|
|
|
required this.controls,
|
|
|
|
|
required this.isFollowing,
|
|
|
|
|
required this.isNorthUp,
|
2026-05-24 14:50:31 +02:00
|
|
|
required this.currentRotationRad,
|
2026-05-12 00:14:11 +02:00
|
|
|
required this.onZoomIn,
|
|
|
|
|
required this.onZoomOut,
|
|
|
|
|
required this.onCenterOnGps,
|
|
|
|
|
required this.onResetNorth,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Positioned(
|
|
|
|
|
right: 10,
|
2026-05-24 14:50:31 +02:00
|
|
|
bottom: 80,
|
2026-05-12 00:14:11 +02:00
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
2026-05-24 14:50:31 +02:00
|
|
|
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),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}),
|
2026-05-12 00:14:11 +02:00
|
|
|
if (controls.showNorthButton) const SizedBox(height: 6),
|
2026-05-24 14:50:31 +02:00
|
|
|
if (controls.showFollowButton && isFollowing != null)
|
2026-05-12 00:14:11 +02:00
|
|
|
Obx(() => _ControlButton(
|
2026-05-24 14:50:31 +02:00
|
|
|
tooltip: isFollowing!.value
|
2026-05-12 00:14:11 +02:00
|
|
|
? 'GPS követés aktív'
|
|
|
|
|
: 'GPS követés kikapcsolva',
|
2026-05-24 14:50:31 +02:00
|
|
|
active: isFollowing!.value,
|
|
|
|
|
icon: isFollowing!.value
|
|
|
|
|
? Icons.gps_fixed
|
|
|
|
|
: Icons.gps_not_fixed,
|
2026-05-12 00:14:11 +02:00
|
|
|
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;
|
2026-05-24 14:50:31 +02:00
|
|
|
final VoidCallback? onTap;
|
2026-05-12 00:14:11 +02:00
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 14:50:31 +02:00
|
|
|
class LabeledMarker extends StatelessWidget {
|
2026-05-12 00:14:11 +02:00
|
|
|
final String label;
|
|
|
|
|
final IconData icon;
|
|
|
|
|
final Color color;
|
|
|
|
|
final Color? activeColor;
|
|
|
|
|
final String? sublabel;
|
|
|
|
|
|
2026-05-24 14:50:31 +02:00
|
|
|
const LabeledMarker({
|
|
|
|
|
super.key,
|
2026-05-12 00:14:11 +02:00
|
|
|
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),
|
2026-05-24 14:50:31 +02:00
|
|
|
),
|
2026-05-12 00:14:11 +02:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
label,
|
|
|
|
|
style: const TextStyle(
|
2026-05-24 14:50:31 +02:00
|
|
|
color: Colors.white,
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
),
|
2026-05-12 00:14:11 +02:00
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
),
|
|
|
|
|
if (sublabel != null)
|
2026-05-24 14:50:31 +02:00
|
|
|
Text(
|
|
|
|
|
sublabel!,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: Colors.white.withOpacity(0.9),
|
|
|
|
|
fontSize: 9,
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-05-12 00:14:11 +02:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Container(width: 2, height: 6, color: c),
|
2026-05-24 14:50:31 +02:00
|
|
|
Icon(
|
|
|
|
|
icon,
|
|
|
|
|
color: c,
|
|
|
|
|
size: 22,
|
|
|
|
|
shadows: const [
|
|
|
|
|
Shadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-05-12 00:14:11 +02:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 14:50:31 +02:00
|
|
|
class PulsingDot extends StatefulWidget {
|
2026-05-12 00:14:11 +02:00
|
|
|
final Color color;
|
2026-05-24 14:50:31 +02:00
|
|
|
const PulsingDot({super.key, required this.color});
|
2026-05-12 00:14:11 +02:00
|
|
|
|
|
|
|
|
@override
|
2026-05-24 14:50:31 +02:00
|
|
|
State<PulsingDot> createState() => _PulsingDotState();
|
2026-05-12 00:14:11 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-24 14:50:31 +02:00
|
|
|
class _PulsingDotState extends State<PulsingDot>
|
2026-05-12 00:14:11 +02:00
|
|
|
with SingleTickerProviderStateMixin {
|
|
|
|
|
late AnimationController _ctrl;
|
|
|
|
|
late Animation<double> _scale;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_ctrl = AnimationController(
|
2026-05-24 14:50:31 +02:00
|
|
|
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),
|
|
|
|
|
);
|
2026-05-12 00:14:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@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(
|
2026-05-24 14:50:31 +02:00
|
|
|
color: widget.color.withOpacity(0.5),
|
|
|
|
|
blurRadius: 8,
|
|
|
|
|
spreadRadius: 2,
|
|
|
|
|
),
|
2026-05-12 00:14:11 +02:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|