Online tracking, deviceidentityservice

This commit is contained in:
2026-06-23 15:21:20 +02:00
parent 65b355edd9
commit aa78c7bb6f
13 changed files with 850 additions and 33 deletions
@@ -40,12 +40,14 @@ import 'package:terepi_seged/pages/ntrip_settings/presentation/views/ntrip_setti
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
import 'package:terepi_seged/services/app_database.dart';
import 'package:terepi_seged/services/coord_converter_service.dart';
import 'package:terepi_seged/services/device_identity_service.dart';
import 'package:terepi_seged/services/gnss/gnss_connection.dart';
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
import 'package:terepi_seged/services/gnss/gnss_service.dart';
import 'package:terepi_seged/services/ntrip_service.dart';
import 'package:terepi_seged/services/project_service.dart';
import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart';
import 'package:terepi_seged/widgets/map/team_member_widget.dart';
import 'package:terepi_seged/widgets/map_edit_tools/color_row.dart';
import 'package:terepi_seged/widgets/map_edit_tools/map_feature_save_sheet.dart';
import 'package:terepi_seged/widgets/map_edit_tools/note_item_list_sheet.dart';
@@ -190,6 +192,10 @@ class MapSurveyController extends GetxController {
final polylineNotes = <Polyline<int>>[].obs;
final polygonNotes = <Polygon<int>>[].obs;
final teamTrackMarkers = <String, Marker>{}.obs;
final teamTrackPoints = <String, List<LatLng>>{}.obs;
RealtimeChannel? _teamChannel;
late final PolygonEditorController polygonEditorController;
final activeEditColor = const Color(0xFF185FA5).obs;
@@ -284,6 +290,8 @@ class MapSurveyController extends GetxController {
await _loadNoteItems();
await _loadMeasurePoints();
_subscribeToTeamPosition();
}
@override
@@ -292,6 +300,7 @@ class MapSurveyController extends GetxController {
_gnssUpdateSub?.cancel();
final f = _supaChannel?.unsubscribe();
if (f != null) unawaited(f);
unawaited(_teamChannel?.unsubscribe());
pointIdController.dispose();
pointDescriptionController.dispose();
@@ -1915,4 +1924,65 @@ class MapSurveyController extends GetxController {
Get.back(); // ← dialóg bezárása, akár sikerült akár nem
}
}
void _subscribeToTeamPosition() {
_teamChannel = Supabase.instance.client
.channel('public:terepi_seged_device_positions')
.onPostgresChanges(
event: PostgresChangeEvent.all,
schema: 'public',
table: 'terepi_seged_device_positions',
callback: (payload) => _onTeamUpdate(payload.newRecord))
.subscribe();
}
void _onTeamUpdate(Map<String, dynamic> data) {
final deviceId = data['device_id'] as String? ?? '';
if (deviceId.isEmpty) return;
if (deviceId == DeviceIdentityService.to.deviceId) return;
final isActive = data['is_active'] as bool? ?? true;
if (!isActive) {
teamTrackMarkers.remove(deviceId);
teamTrackPoints.remove(deviceId);
teamTrackMarkers.refresh();
teamTrackPoints.refresh();
return;
}
final lat = (data['latitude'] as num?)?.toDouble();
final lon = (data['longitude'] as num?)?.toDouble();
final name = data['user_name'] as String? ?? deviceId.substring(0, 8);
if (lat == null || lon == null) return;
final point = LatLng(lat, lon);
teamTrackMarkers[deviceId] = _buildTeamMarker(point, name, deviceId);
teamTrackPoints.putIfAbsent(deviceId, () => []);
teamTrackPoints[deviceId]!.add(point);
teamTrackMarkers.refresh();
teamTrackPoints.refresh();
}
Marker _buildTeamMarker(LatLng point, String name, String deviceId) {
final colors = [
Colors.blue,
Colors.purple,
Colors.teal,
Colors.indigo,
Colors.cyan,
Colors.deepPurple
];
final color = colors[deviceId.hashCode.abs() % colors.length];
return Marker(
point: point,
width: 100,
height: 48,
alignment: Alignment.bottomCenter,
child: TeamMemberWidget(name: name, color: color));
}
}
@@ -85,13 +85,37 @@ class MapSurveyView extends GetView<MapSurveyController> {
layers: [
const ImportedLayerOverlay(),
// Track polyline
Obx(() {
final inTrackMode = controller.mode.value == MapSurveyMode.track;
if (!inTrackMode) return const SizedBox.shrink();
final ids = TrackingController.to.overlayTrackIds;
if (ids.isEmpty) return const SizedBox.shrink();
final polylines = ids
.map((id) {
final pts = TrackingController.to.getCoordsFor(id);
if (pts.isEmpty) return null;
return Polyline(
points: pts,
color: Colors.blue.withOpacity(0.75),
strokeWidth: 2.5,
borderColor: Colors.white.withOpacity(0.4),
borderStrokeWidth: 1.0,
);
})
.whereType<Polyline>()
.toList();
if (polylines.isEmpty) return const SizedBox.shrink();
return PolylineLayer(polylines: polylines);
}),
Obx(() {
final isTracking = TrackingController.to.isRecording.value;
final inTrackMode = controller.mode.value == MapSurveyMode.track;
if (!isTracking && !inTrackMode) {
return const SizedBox.shrink();
} else {
return _buildTrackLayer();
return _buildTrackLayer1();
}
}),
Obx(() {
@@ -175,10 +199,37 @@ class MapSurveyView extends GetView<MapSurveyController> {
NoteItemLabelLayer(
controller: controller,
),
Obx(() {
final tracks = controller.teamTrackPoints;
if (tracks.isEmpty) return const SizedBox.shrink();
return PolylineLayer(
polylines: tracks.entries.map((e) {
final colors = [
Colors.blue,
Colors.purple,
Colors.teal,
Colors.indigo,
Colors.cyan,
Colors.deepPurple
];
final color = colors[e.key.hashCode.abs() % colors.length];
return Polyline(
points: e.value,
color: color.withValues(alpha: 0.7),
strokeWidth: 2.5);
}).toList(),
);
}),
Obx(() {
final markers =
List<Marker>.from(controller.teamTrackMarkers.values);
if (markers.isEmpty) return const SizedBox.shrink();
return MarkerLayer(markers: markers);
}),
Obx(() {
final isGpsActive = GnssService.to.activeConnectionType.value !=
GnssConnectionType.none;
if (isGpsActive) {
if (isGpsActive && controller.mode.value != MapSurveyMode.track) {
return MarkerLayer(
markers: controller.currentLocationMarker.toList());
}
@@ -276,6 +327,43 @@ class MapSurveyView extends GetView<MapSurveyController> {
]);
});
}
Widget _buildTrackLayer1() {
return Obx(() {
final ctrl = TrackingController.to;
final points = ctrl.livePoints.toList();
if (points.isEmpty) return const SizedBox.shrink();
return Stack(children: [
// 1. Track vonal
PolylineLayer(polylines: [
Polyline(
points: points,
color: Colors.red.withOpacity(0.85),
strokeWidth: 3.0,
),
]),
// 2. Markerek a vonal felett
MarkerLayer(markers: [
// Kezdőpont — zöld
Marker(
point: points.first,
child: const Icon(Icons.flag, color: Colors.green, size: 28),
),
// Utolsó pont — piros (ha van legalább 2 pont)
if (points.length > 1)
Marker(
point: points.last,
child: _PulsingDot(
color: TrackingController.to.isPaused.value
? Colors.orange
: Colors.blue,
),
),
]),
]);
});
}
}
class _ModeSelector extends GetView<MapSurveyController> {
@@ -302,6 +390,58 @@ class _ModeSelector extends GetView<MapSurveyController> {
}
}
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 _anim;
late Animation<double> _scale;
@override
void initState() {
super.initState();
_anim = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000))
..repeat(reverse: true);
_scale = Tween(begin: 0.8, end: 1.1)
.animate(CurvedAnimation(parent: _anim, curve: Curves.easeInOut));
}
@override
void dispose() {
_anim.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scale,
child: Container(
width: 8,
height: 8,
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: 4,
spreadRadius: 2)
],
),
),
);
}
}
class _StakeoutPanel extends GetView<MapSurveyController> {
const _StakeoutPanel();
@@ -6,6 +6,7 @@ import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart';
import 'package:share_plus/share_plus.dart';
import 'package:terepi_seged/services/project_service.dart';
import 'package:terepi_seged/services/track_sync_service.dart';
import '../../../../services/location_source.dart';
import '../../../../services/phone_gps_source.dart';
@@ -150,6 +151,20 @@ class TrackingController extends GetxController {
callback: startTrackingCallback,
);
if (!(currentTrack.value?.isLocalOnly ?? true)) {
TrackSyncService.to
.createRemoteTrack(currentTrack.value!)
.then((supabaseId) async {
if (supabaseId == null) return;
final updated = currentTrack.value!.copyWith(supabaseId: supabaseId);
await _db.updateTrack(updated);
currentTrack.value = updated;
TrackSyncService.to.startSession(updated);
});
} else {
TrackSyncService.to.startSession(currentTrack.value!);
}
// GPS stream feliratkozás
_positionSub = _source!.positionStream.listen(
_onPosition,
@@ -207,6 +222,7 @@ class TrackingController extends GetxController {
pointCount: livePoints.length,
);
await _db.updateTrack(finished);
await TrackSyncService.to.stopSession(finished);
currentTrack.value = finished;
}
@@ -236,7 +252,7 @@ class TrackingController extends GetxController {
try {
final path = await _exporter.export(track);
await Share.shareXFiles([XFile(path)],
subject: 'Nyomvonal: ${track.name}');
subject: 'Nyomvonal: ${track.name}.gpx');
} catch (e) {
Get.snackbar('Export hiba', e.toString(),
backgroundColor: Colors.red, colorText: Colors.white);
@@ -289,6 +305,8 @@ class TrackingController extends GetxController {
);
await _db.addPoint(point, _accumulatedDistance);
TrackSyncService.to.onNewPoint(point);
_lastPoint = point;
// UI frissítés