mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-05-26 01:47:55 +02:00
More camera -> node, notifications for approaching
This commit is contained in:
@@ -7,7 +7,7 @@ import '../services/map_data_provider.dart';
|
||||
import '../services/node_cache.dart';
|
||||
import '../services/network_status.dart';
|
||||
import '../models/node_profile.dart';
|
||||
import '../models/osm_camera_node.dart';
|
||||
import '../models/osm_node.dart';
|
||||
import '../app_state.dart';
|
||||
|
||||
/// Provides surveillance nodes for a map view, using an in-memory cache and optionally
|
||||
@@ -21,7 +21,7 @@ class CameraProviderWithCache extends ChangeNotifier {
|
||||
|
||||
/// Call this to get (quickly) all cached overlays for the given view.
|
||||
/// Filters by currently enabled profiles.
|
||||
List<OsmCameraNode> getCachedNodesForBounds(LatLngBounds bounds) {
|
||||
List<OsmNode> getCachedNodesForBounds(LatLngBounds bounds) {
|
||||
final allNodes = NodeCache.instance.queryByBounds(bounds);
|
||||
final enabledProfiles = AppState.instance.enabledProfiles;
|
||||
|
||||
@@ -79,7 +79,7 @@ class CameraProviderWithCache extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Check if a node matches any of the provided profiles
|
||||
bool _matchesAnyProfile(OsmCameraNode node, List<NodeProfile> profiles) {
|
||||
bool _matchesAnyProfile(OsmNode node, List<NodeProfile> profiles) {
|
||||
for (final profile in profiles) {
|
||||
if (_nodeMatchesProfile(node, profile)) return true;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class CameraProviderWithCache extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Check if a node matches a specific profile (all profile tags must match)
|
||||
bool _nodeMatchesProfile(OsmCameraNode node, NodeProfile profile) {
|
||||
bool _nodeMatchesProfile(OsmNode node, NodeProfile profile) {
|
||||
for (final entry in profile.tags.entries) {
|
||||
if (node.tags[entry.key] != entry.value) return false;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../../dev_config.dart';
|
||||
import '../../models/osm_camera_node.dart';
|
||||
import '../../models/osm_node.dart';
|
||||
import '../node_tag_sheet.dart';
|
||||
import '../camera_icon.dart';
|
||||
|
||||
/// Smart marker widget for camera with single/double tap distinction
|
||||
class CameraMapMarker extends StatefulWidget {
|
||||
final OsmCameraNode node;
|
||||
final OsmNode node;
|
||||
final MapController mapController;
|
||||
const CameraMapMarker({required this.node, required this.mapController, Key? key}) : super(key: key);
|
||||
|
||||
@@ -76,7 +76,7 @@ class _CameraMapMarkerState extends State<CameraMapMarker> {
|
||||
/// Helper class to build marker layers for cameras and user location
|
||||
class CameraMarkersBuilder {
|
||||
static List<Marker> buildCameraMarkers({
|
||||
required List<OsmCameraNode> cameras,
|
||||
required List<OsmNode> cameras,
|
||||
required MapController mapController,
|
||||
LatLng? userLocation,
|
||||
}) {
|
||||
@@ -104,7 +104,7 @@ class CameraMarkersBuilder {
|
||||
return markers;
|
||||
}
|
||||
|
||||
static bool _isValidCameraCoordinate(OsmCameraNode node) {
|
||||
static bool _isValidCameraCoordinate(OsmNode node) {
|
||||
return (node.coord.latitude != 0 || node.coord.longitude != 0) &&
|
||||
node.coord.latitude.abs() <= 90 &&
|
||||
node.coord.longitude.abs() <= 180;
|
||||
|
||||
@@ -5,12 +5,12 @@ import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../../app_state.dart';
|
||||
import '../../dev_config.dart';
|
||||
import '../../models/osm_camera_node.dart';
|
||||
import '../../models/osm_node.dart';
|
||||
|
||||
/// Helper class to build direction cone polygons for cameras
|
||||
class DirectionConesBuilder {
|
||||
static List<Polygon> buildDirectionCones({
|
||||
required List<OsmCameraNode> cameras,
|
||||
required List<OsmNode> cameras,
|
||||
required double zoom,
|
||||
AddNodeSession? session,
|
||||
EditNodeSession? editSession,
|
||||
@@ -52,7 +52,7 @@ class DirectionConesBuilder {
|
||||
return overlays;
|
||||
}
|
||||
|
||||
static bool _isValidCameraWithDirection(OsmCameraNode node) {
|
||||
static bool _isValidCameraWithDirection(OsmNode node) {
|
||||
return node.hasDirection &&
|
||||
node.directionDeg != null &&
|
||||
(node.coord.latitude != 0 || node.coord.longitude != 0) &&
|
||||
@@ -60,7 +60,7 @@ class DirectionConesBuilder {
|
||||
node.coord.longitude.abs() <= 180;
|
||||
}
|
||||
|
||||
static bool _isPendingUpload(OsmCameraNode node) {
|
||||
static bool _isPendingUpload(OsmNode node) {
|
||||
return node.tags.containsKey('_pending_upload') &&
|
||||
node.tags['_pending_upload'] == 'true';
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../../dev_config.dart';
|
||||
import '../../app_state.dart' show FollowMeMode;
|
||||
import '../../services/proximity_alert_service.dart';
|
||||
import '../../models/osm_node.dart';
|
||||
import '../../models/node_profile.dart';
|
||||
|
||||
/// Manages GPS location tracking, follow-me modes, and location-based map animations.
|
||||
/// Handles GPS permissions, position streams, and follow-me behavior.
|
||||
@@ -81,6 +84,11 @@ class GpsController {
|
||||
required FollowMeMode followMeMode,
|
||||
required AnimatedMapController controller,
|
||||
required VoidCallback onLocationUpdated,
|
||||
// Optional parameters for proximity alerts
|
||||
bool proximityAlertsEnabled = false,
|
||||
int proximityAlertDistance = 200,
|
||||
List<OsmNode> nearbyNodes = const [],
|
||||
List<NodeProfile> enabledProfiles = const [],
|
||||
}) {
|
||||
final latLng = LatLng(position.latitude, position.longitude);
|
||||
_currentLatLng = latLng;
|
||||
@@ -88,6 +96,16 @@ class GpsController {
|
||||
// Notify that location was updated (for setState, etc.)
|
||||
onLocationUpdated();
|
||||
|
||||
// Check proximity alerts if enabled
|
||||
if (proximityAlertsEnabled && nearbyNodes.isNotEmpty) {
|
||||
ProximityAlertService().checkProximity(
|
||||
userLocation: latLng,
|
||||
nodes: nearbyNodes,
|
||||
enabledProfiles: enabledProfiles,
|
||||
alertDistance: proximityAlertDistance,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle follow-me animations if enabled - use current mode from app state
|
||||
if (followMeMode != FollowMeMode.off) {
|
||||
debugPrint('[GpsController] GPS position update: ${latLng.latitude}, ${latLng.longitude}, follow-me: $followMeMode');
|
||||
@@ -131,6 +149,10 @@ class GpsController {
|
||||
required AnimatedMapController controller,
|
||||
required VoidCallback onLocationUpdated,
|
||||
required FollowMeMode Function() getCurrentFollowMeMode,
|
||||
required bool Function() getProximityAlertsEnabled,
|
||||
required int Function() getProximityAlertDistance,
|
||||
required List<OsmNode> Function() getNearbyNodes,
|
||||
required List<NodeProfile> Function() getEnabledProfiles,
|
||||
}) async {
|
||||
final perm = await Geolocator.requestPermission();
|
||||
if (perm == LocationPermission.denied ||
|
||||
@@ -142,11 +164,20 @@ class GpsController {
|
||||
_positionSub = Geolocator.getPositionStream().listen((Position position) {
|
||||
// Get the current follow-me mode from the app state each time
|
||||
final currentFollowMeMode = getCurrentFollowMeMode();
|
||||
final proximityAlertsEnabled = getProximityAlertsEnabled();
|
||||
final proximityAlertDistance = getProximityAlertDistance();
|
||||
final nearbyNodes = getNearbyNodes();
|
||||
final enabledProfiles = getEnabledProfiles();
|
||||
|
||||
processPositionUpdate(
|
||||
position: position,
|
||||
followMeMode: currentFollowMeMode,
|
||||
controller: controller,
|
||||
onLocationUpdated: onLocationUpdated,
|
||||
proximityAlertsEnabled: proximityAlertsEnabled,
|
||||
proximityAlertDistance: proximityAlertDistance,
|
||||
nearbyNodes: nearbyNodes,
|
||||
enabledProfiles: enabledProfiles,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:provider/provider.dart';
|
||||
import '../app_state.dart';
|
||||
import '../services/offline_area_service.dart';
|
||||
import '../services/network_status.dart';
|
||||
import '../models/osm_camera_node.dart';
|
||||
import '../models/osm_node.dart';
|
||||
import '../models/node_profile.dart';
|
||||
import '../models/tile_provider.dart';
|
||||
import 'debouncer.dart';
|
||||
@@ -21,8 +21,10 @@ import 'map/tile_layer_manager.dart';
|
||||
import 'map/camera_refresh_controller.dart';
|
||||
import 'map/gps_controller.dart';
|
||||
import 'network_status_indicator.dart';
|
||||
import 'proximity_alert_banner.dart';
|
||||
import '../dev_config.dart';
|
||||
import '../app_state.dart' show FollowMeMode;
|
||||
import '../services/proximity_alert_service.dart';
|
||||
|
||||
class MapView extends StatefulWidget {
|
||||
final AnimatedMapController controller;
|
||||
@@ -55,6 +57,9 @@ class MapViewState extends State<MapView> {
|
||||
|
||||
// Track zoom to clear queue on zoom changes
|
||||
double? _lastZoom;
|
||||
|
||||
// State for proximity alert banner
|
||||
bool _showProximityBanner = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -68,6 +73,17 @@ class MapViewState extends State<MapView> {
|
||||
_cameraController.initialize(onCamerasUpdated: _onCamerasUpdated);
|
||||
_gpsController = GpsController();
|
||||
|
||||
// Initialize proximity alert service
|
||||
ProximityAlertService().initialize(
|
||||
onVisualAlert: () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_showProximityBanner = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Load last map position before initializing GPS
|
||||
_positionManager.loadLastMapPosition().then((_) {
|
||||
// Move to last known position after loading and widget is built
|
||||
@@ -93,6 +109,59 @@ class MapViewState extends State<MapView> {
|
||||
}
|
||||
return FollowMeMode.off;
|
||||
},
|
||||
getProximityAlertsEnabled: () {
|
||||
if (mounted) {
|
||||
try {
|
||||
return context.read<AppState>().proximityAlertsEnabled;
|
||||
} catch (e) {
|
||||
debugPrint('[MapView] Could not read proximity alerts enabled: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
getProximityAlertDistance: () {
|
||||
if (mounted) {
|
||||
try {
|
||||
return context.read<AppState>().proximityAlertDistance;
|
||||
} catch (e) {
|
||||
debugPrint('[MapView] Could not read proximity alert distance: $e');
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
return 200;
|
||||
},
|
||||
getNearbyNodes: () {
|
||||
if (mounted) {
|
||||
try {
|
||||
final cameraProvider = context.read<CameraProviderWithCache>();
|
||||
LatLngBounds? mapBounds;
|
||||
try {
|
||||
mapBounds = _controller.mapController.camera.visibleBounds;
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
return mapBounds != null
|
||||
? cameraProvider.getCachedNodesForBounds(mapBounds)
|
||||
: [];
|
||||
} catch (e) {
|
||||
debugPrint('[MapView] Could not get nearby nodes: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
},
|
||||
getEnabledProfiles: () {
|
||||
if (mounted) {
|
||||
try {
|
||||
return context.read<AppState>().enabledProfiles;
|
||||
} catch (e) {
|
||||
debugPrint('[MapView] Could not read enabled profiles: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
},
|
||||
);
|
||||
|
||||
// Fetch initial cameras
|
||||
@@ -265,7 +334,7 @@ class MapViewState extends State<MapView> {
|
||||
}
|
||||
final cameras = (mapBounds != null)
|
||||
? cameraProvider.getCachedNodesForBounds(mapBounds)
|
||||
: <OsmCameraNode>[];
|
||||
: <OsmNode>[];
|
||||
|
||||
final markers = CameraMarkersBuilder.buildCameraMarkers(
|
||||
cameras: cameras,
|
||||
@@ -402,12 +471,22 @@ class MapViewState extends State<MapView> {
|
||||
|
||||
// Network status indicator (top-left)
|
||||
const NetworkStatusIndicator(),
|
||||
|
||||
// Proximity alert banner (top)
|
||||
ProximityAlertBanner(
|
||||
isVisible: _showProximityBanner,
|
||||
onDismiss: () {
|
||||
setState(() {
|
||||
_showProximityBanner = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Build polylines connecting original cameras to their edited positions
|
||||
List<Polyline> _buildEditLines(List<OsmCameraNode> cameras) {
|
||||
List<Polyline> _buildEditLines(List<OsmNode> cameras) {
|
||||
final lines = <Polyline>[];
|
||||
|
||||
// Create a lookup map of original node IDs to their coordinates
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/osm_camera_node.dart';
|
||||
import '../models/osm_node.dart';
|
||||
import '../app_state.dart';
|
||||
import '../services/localization_service.dart';
|
||||
|
||||
class NodeTagSheet extends StatelessWidget {
|
||||
final OsmCameraNode node;
|
||||
final OsmNode node;
|
||||
|
||||
const NodeTagSheet({super.key, required this.node});
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Simple red banner that flashes briefly when proximity alert is triggered
|
||||
/// Follows brutalist principles: simple, explicit functionality
|
||||
class ProximityAlertBanner extends StatefulWidget {
|
||||
final bool isVisible;
|
||||
final VoidCallback? onDismiss;
|
||||
|
||||
const ProximityAlertBanner({
|
||||
super.key,
|
||||
required this.isVisible,
|
||||
this.onDismiss,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProximityAlertBanner> createState() => _ProximityAlertBannerState();
|
||||
}
|
||||
|
||||
class _ProximityAlertBannerState extends State<ProximityAlertBanner>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ProximityAlertBanner oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget.isVisible != oldWidget.isVisible) {
|
||||
if (widget.isVisible) {
|
||||
_controller.forward();
|
||||
// Auto-hide after 3 seconds
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (mounted) {
|
||||
_controller.reverse().then((_) {
|
||||
widget.onDismiss?.call();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_controller.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
if (_animation.value == 0.0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
top: MediaQuery.of(context).padding.top,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, -60 * (1 - _animation.value)),
|
||||
child: Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade600,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_controller.reverse().then((_) {
|
||||
widget.onDismiss?.call();
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.warning,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Surveillance device nearby',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user