Térkép refraktorálás

This commit is contained in:
torok.istvan 2026-05-17 00:35:21 +02:00
parent d50a324e44
commit ee0f90e247
5 changed files with 499 additions and 98 deletions

View File

@ -12,6 +12,7 @@ import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_surv
import 'package:terepi_seged/pages/map_survey/presentations/views/settings_dialog.dart';
import 'package:terepi_seged/utils/rive_utils.dart';
import 'package:terepi_seged/widgets/coordinate_panel.dart';
import 'package:terepi_seged/widgets/map_bottom_panel.dart';
import 'package:terepi_seged/widgets/save_point_fab.dart';
import 'package:terepi_seged/widgets/shared_map_widgets.dart';
@ -26,26 +27,31 @@ class MapSurveyView extends GetView<MapSurveyController> {
const SharedMapWidget(),
Positioned(
top: 8,
right: 8,
right: 60,
left: 8,
child: CoordinatePanel.fromController(controller),
),
Positioned(top: 8, left: 0, right: 0, child: _ModeSelector()),
// Positioned(top: 8, left: 0, right: 0, child: _ModeSelector()),
// Positioned(
// bottom: 80,
// left: 8,
// right: 8,
// child: Obx(
// () => controller.mode.value == MapSurveyMode.stakeout
// ? _StakeoutPanel() // ΔY, ΔX, távolság, irányszög
// : const SizedBox.shrink(),
// ),
// ),
// Positioned(
// bottom: 16,
// right: 16,
// child: SavePointFab(controller: controller),
// ),
Positioned(
bottom: 80,
left: 8,
right: 8,
child: Obx(
() => controller.mode.value == MapSurveyMode.stakeout
? _StakeoutPanel() // ΔY, ΔX, távolság, irányszög
: const SizedBox.shrink(),
),
),
Positioned(
bottom: 16,
right: 16,
child: SavePointFab(controller: controller),
),
bottom: 0,
left: 0,
right: 0,
child: MapBottomPanel(controller: controller))
]);
}
}

View File

@ -4,6 +4,7 @@ class ShellController extends GetxController {
static ShellController get to => Get.find();
final currentIndex = 0.obs;
final isNavBarVisible = true.obs;
static const titles = [
'Térkép',
@ -24,4 +25,8 @@ class ShellController extends GetxController {
void goToSurvey() => goToTab(1);
void goToTracking() => goToTab(2);
void goToData() => goToTab(3);
void showNavBar() => isNavBarVisible.value = true;
void hideNavBar() => isNavBarVisible.value = false;
void toggleNavBar() => isNavBarVisible.value = !isNavBarVisible.value;
}

View File

@ -46,7 +46,12 @@ class ShellView extends GetView<ShellController> {
index: controller.currentIndex.value,
children: _pages,
)),
bottomNavigationBar: Obx(() => NavigationBar(
bottomNavigationBar: Obx(
() => AnimatedSize(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: controller.isNavBarVisible.value
? NavigationBar(
selectedIndex: controller.currentIndex.value,
onDestinationSelected: controller.goToTab,
destinations: const [
@ -69,7 +74,9 @@ class ShellView extends GetView<ShellController> {
label: 'Adatok',
),
],
)),
)
: const SizedBox.shrink()),
),
);
}
}

View File

@ -27,66 +27,23 @@ class GnssStatusChip extends StatelessWidget {
return Obx(() {
final connState = GnssService.to.connectionState.value;
final quality = GnssService.to.gpsQuality.value;
final device = GnssDeviceService.to.selectedDevice.value;
final sats = GnssService.to.satelliteCount.value;
final isConnected = connState == GnssConnectionState.connected;
final isConn = connState == GnssConnectionState.connected;
final isConnecting = connState == GnssConnectionState.connecting;
final color = _chipColor(isConnected, quality);
final label = _chipLabel(connState, quality, device?.name);
final color = _chipColor(isConn, quality);
return GestureDetector(
onTap: () => GnssDevicePickerDialog.show(),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.5)),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
// Állapot ikon
if (isConnecting)
SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(
strokeWidth: 1.5,
color: color,
),
return Tooltip(
// Hosszú nyomásra szöveg jelenik meg
message: _chipLabel(connState, quality, ''),
child: IconButton(
icon: isConnecting
? SizedBox(
width: 18,
height: 18,
child:
CircularProgressIndicator(strokeWidth: 2, color: color),
)
else
Icon(
_chipIcon(isConnected, quality),
size: 12,
color: color,
),
const SizedBox(width: 4),
// Felirat
Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: color,
),
),
// Műholdak száma (csak ha van fix)
if (isConnected && quality > 0) ...[
const SizedBox(width: 4),
Text(
'($sats)',
style: TextStyle(
fontSize: 10,
color: color.withOpacity(0.8),
),
),
],
]),
: Icon(_chipIcon(isConn, quality), color: color),
onPressed: () => GnssDevicePickerDialog.show(),
),
);
});

View File

@ -0,0 +1,426 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
import 'package:terepi_seged/pages/shell/presentations/controllers/shell_controller.dart';
import '../services/gnss/gnss_service.dart';
/// Összecsukható alsó panel bemérés és kitűzés módváltóval.
///
/// Összecsukva: mód chip-ek + kis mentés gomb (nem takarja a térképet).
/// Kitűzés módban automatikusan kinyílik a ΔY/ΔX/távolság panellel.
///
/// Használat a Stack aljában:
/// ```dart
/// Positioned(
/// bottom: 0, left: 0, right: 0,
/// child: SurveyBottomPanel(controller: controller),
/// )
/// ```
class MapBottomPanel extends StatefulWidget {
final dynamic controller;
const MapBottomPanel({super.key, required this.controller});
@override
State<MapBottomPanel> createState() => _SurveyBottomPanelState();
}
class _SurveyBottomPanelState extends State<MapBottomPanel>
with SingleTickerProviderStateMixin {
late AnimationController _animCtrl;
late Animation<double> _heightAnim;
// Panel magasságok
static const _collapsedHeight = 68.0;
static const _expandedHeight = 220.0;
bool _isExpanded = false;
@override
void initState() {
super.initState();
_animCtrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 250),
);
_heightAnim = Tween(
begin: _collapsedHeight,
end: _expandedHeight,
).animate(CurvedAnimation(
parent: _animCtrl,
curve: Curves.easeOutCubic,
));
// Ha kitűzés módba lépünk automatikus kinyitás
ever(widget.controller.mode as Rx<MapSurveyMode>, (mode) {
if (mode == MapSurveyMode.stakeout.index && !_isExpanded) {
_expand();
}
});
}
@override
void dispose() {
_animCtrl.dispose();
super.dispose();
}
void _toggle() {
if (_isExpanded) {
_collapse();
} else {
_expand();
}
}
void _expand() {
_animCtrl.forward();
setState(() => _isExpanded = true);
ShellController.to.hideNavBar();
}
void _collapse() {
_animCtrl.reverse();
setState(() => _isExpanded = false);
ShellController.to.showNavBar();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _heightAnim,
builder: (context, child) => Container(
height: _heightAnim.value,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.88),
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle húzásra nyíl/csuk
GestureDetector(
onTap: _toggle,
onVerticalDragEnd: (d) {
// Felfelé húzás kinyit, lefelé csuk
if (d.primaryVelocity! < -100) _expand();
if (d.primaryVelocity! > 100) _collapse();
},
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: double.infinity,
height: 20,
child: Center(
child: Container(
width: 36,
height: 3,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.25),
borderRadius: BorderRadius.circular(2),
),
),
),
),
),
// Módváltó + mentés gomb (mindig látható)
Padding(
padding: EdgeInsets.fromLTRB(
10, 6, 10, 6 + MediaQuery.of(context).padding.bottom),
child: _ModeRow(
controller: widget.controller,
onSave: () => _showSaveDialog(context),
),
),
// Kitűzési adatok (csak kinyitva)
if (_isExpanded && _animCtrl.value > 0.5)
Expanded(
child: _StakeoutContent(
controller: widget.controller,
onSave: () => _showSaveDialog(context),
),
),
],
),
),
),
);
}
void _showSaveDialog(BuildContext context) {
widget.controller.showAddPointDialog();
}
}
// Módváltó sor
class _ModeRow extends StatelessWidget {
final dynamic controller;
final VoidCallback onSave;
const _ModeRow({required this.controller, required this.onSave});
@override
Widget build(BuildContext context) {
return Obx(() {
final mode = (controller.mode?.value ?? MapSurveyMode.measure);
return Row(children: [
// Bemérés chip
Expanded(
child: _ModeChip(
label: 'Bemérés',
icon: Icons.gps_fixed,
selected: mode == MapSurveyMode.measure,
onTap: () => controller.switchMode?.call(MapSurveyMode.measure),
),
),
const SizedBox(width: 8),
// Kitűzés chip
Expanded(
child: _ModeChip(
label: 'Kitűzés',
icon: Icons.flag_outlined,
selected: mode == MapSurveyMode.stakeout,
onTap: () => controller.switchMode?.call(MapSurveyMode.stakeout),
),
),
const SizedBox(width: 8),
// Mentés gomb bemérés módban kis gomb
if (mode == MapSurveyMode.measure)
GestureDetector(
onTap: onSave,
child: Obx(() {
final hasfix = GnssService.to.gpsQuality.value > 0;
return Container(
width: 44,
height: 32,
decoration: BoxDecoration(
color: hasfix ? Colors.blue.shade700 : Colors.grey.shade800,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.location_on,
color: hasfix ? Colors.white : Colors.grey,
size: 20,
),
);
}),
),
]);
});
}
}
// Módváltó chip
class _ModeChip extends StatelessWidget {
final String label;
final IconData icon;
final bool selected;
final VoidCallback onTap;
const _ModeChip({
required this.label,
required this.icon,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 32,
decoration: BoxDecoration(
color: selected
? Colors.blue.withOpacity(0.25)
: Colors.white.withOpacity(0.06),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: selected
? Colors.blue.shade300.withOpacity(0.6)
: Colors.white.withOpacity(0.12),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 14,
color: selected ? Colors.blue.shade300 : Colors.white38,
),
const SizedBox(width: 5),
Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: selected ? Colors.blue.shade300 : Colors.white38,
),
),
],
),
),
);
}
}
// Kitűzési tartalom
class _StakeoutContent extends StatelessWidget {
final dynamic controller;
final VoidCallback onSave;
const _StakeoutContent({
required this.controller,
required this.onSave,
});
@override
Widget build(BuildContext context) {
return Obx(() {
final dy = (controller.deltaY as double?) ?? 0.0;
final dx = (controller.deltaX as double?) ?? 0.0;
final dist = (controller.distanceToTarget as double?) ?? 0.0;
final onTarget = (controller.isOnTarget as bool?) ?? false;
final name = (controller.targetName as RxString?) ?? '';
return Padding(
padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ΔY / ΔX / Távolság
Row(children: [
_DeltaCell('ΔY', dy),
const SizedBox(width: 6),
_DeltaCell('ΔX', dx),
const SizedBox(width: 6),
_DeltaCell('Táv', dist, alwaysPositive: true),
]),
const SizedBox(height: 6),
// Célpont státusz
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: onTarget
? Colors.green.withOpacity(0.12)
: Colors.orange.withOpacity(0.10),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: onTarget
? Colors.green.withOpacity(0.4)
: Colors.orange.withOpacity(0.3),
),
),
child: Text(
onTarget
? '✓ Célponton — rögzíthető'
: '$name · ${dist.toStringAsFixed(3)} m a céltól',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: onTarget ? Colors.greenAccent : Colors.orange,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 6),
// Nagy mentés gomb kitűzés módban
SizedBox(
width: double.infinity,
height: 36,
child: FilledButton.icon(
style: FilledButton.styleFrom(
backgroundColor:
onTarget ? Colors.green.shade700 : Colors.blue.shade700,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
icon: const Icon(Icons.location_on, size: 18),
label: const Text('Pont rögzítése',
style: TextStyle(fontSize: 13)),
onPressed: onSave,
),
),
],
),
);
});
}
}
// ΔY / ΔX / Távolság cella
class _DeltaCell extends StatelessWidget {
final String label;
final double value;
final bool alwaysPositive;
const _DeltaCell(this.label, this.value, {this.alwaysPositive = false});
@override
Widget build(BuildContext context) {
final display = alwaysPositive ? value.abs() : value;
final prefix = (!alwaysPositive && value > 0) ? '+' : '';
final color = value.abs() < 0.05
? Colors.greenAccent
: value.abs() < 0.5
? Colors.orange
: Colors.red;
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(6),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: TextStyle(
fontSize: 9,
color: Colors.white.withOpacity(0.4),
),
),
Text(
'$prefix${display.toStringAsFixed(3)} m',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: color,
fontFeatures: const [FontFeature.tabularFigures()],
),
),
],
),
),
);
}
}