Online tracking, deviceidentityservice
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user