mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-13 01:03:03 +00:00
break up map_view
This commit is contained in:
94
lib/widgets/map/camera_markers.dart
Normal file
94
lib/widgets/map/camera_markers.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../../dev_config.dart';
|
||||
import '../../models/osm_camera_node.dart';
|
||||
import '../camera_tag_sheet.dart';
|
||||
|
||||
/// Smart marker widget for camera with single/double tap distinction
|
||||
class CameraMapMarker extends StatefulWidget {
|
||||
final OsmCameraNode node;
|
||||
final MapController mapController;
|
||||
const CameraMapMarker({required this.node, required this.mapController, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CameraMapMarker> createState() => _CameraMapMarkerState();
|
||||
}
|
||||
|
||||
class _CameraMapMarkerState extends State<CameraMapMarker> {
|
||||
Timer? _tapTimer;
|
||||
// From dev_config.dart for build-time parameters
|
||||
static const Duration tapTimeout = kMarkerTapTimeout;
|
||||
|
||||
void _onTap() {
|
||||
_tapTimer = Timer(tapTimeout, () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (_) => CameraTagSheet(node: widget.node),
|
||||
showDragHandle: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _onDoubleTap() {
|
||||
_tapTimer?.cancel();
|
||||
widget.mapController.move(widget.node.coord, widget.mapController.camera.zoom + 1);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tapTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Check if this is a pending upload
|
||||
final isPending = widget.node.tags.containsKey('_pending_upload') &&
|
||||
widget.node.tags['_pending_upload'] == 'true';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: _onTap,
|
||||
onDoubleTap: _onDoubleTap,
|
||||
child: Icon(
|
||||
Icons.videocam,
|
||||
color: isPending ? Colors.purple : Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper class to build marker layers for cameras and user location
|
||||
class CameraMarkersBuilder {
|
||||
static List<Marker> buildCameraMarkers({
|
||||
required List<OsmCameraNode> cameras,
|
||||
required MapController mapController,
|
||||
LatLng? userLocation,
|
||||
}) {
|
||||
final markers = <Marker>[
|
||||
// Camera markers
|
||||
...cameras
|
||||
.where((n) => n.coord.latitude != 0 || n.coord.longitude != 0)
|
||||
.where((n) => n.coord.latitude.abs() <= 90 && n.coord.longitude.abs() <= 180)
|
||||
.map((n) => Marker(
|
||||
point: n.coord,
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CameraMapMarker(node: n, mapController: mapController),
|
||||
)),
|
||||
|
||||
// User location marker
|
||||
if (userLocation != null)
|
||||
Marker(
|
||||
point: userLocation,
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: const Icon(Icons.my_location, color: Colors.blue),
|
||||
),
|
||||
];
|
||||
|
||||
return markers;
|
||||
}
|
||||
}
|
||||
76
lib/widgets/map/direction_cones.dart
Normal file
76
lib/widgets/map/direction_cones.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../../app_state.dart';
|
||||
import '../../dev_config.dart';
|
||||
import '../../models/osm_camera_node.dart';
|
||||
|
||||
/// Helper class to build direction cone polygons for cameras
|
||||
class DirectionConesBuilder {
|
||||
static List<Polygon> buildDirectionCones({
|
||||
required List<OsmCameraNode> cameras,
|
||||
required double zoom,
|
||||
AddCameraSession? session,
|
||||
}) {
|
||||
final overlays = <Polygon>[];
|
||||
|
||||
// Add session cone if in add-camera mode
|
||||
if (session != null && session.target != null) {
|
||||
overlays.add(_buildCone(
|
||||
session.target!,
|
||||
session.directionDegrees,
|
||||
zoom,
|
||||
));
|
||||
}
|
||||
|
||||
// Add cones for cameras with direction
|
||||
overlays.addAll(
|
||||
cameras
|
||||
.where((n) => n.hasDirection && n.directionDeg != null)
|
||||
.where((n) => n.coord.latitude != 0 || n.coord.longitude != 0)
|
||||
.where((n) => n.coord.latitude.abs() <= 90 && n.coord.longitude.abs() <= 180)
|
||||
.map((n) => _buildCone(
|
||||
n.coord,
|
||||
n.directionDeg!,
|
||||
zoom,
|
||||
isPending: n.tags.containsKey('_pending_upload') &&
|
||||
n.tags['_pending_upload'] == 'true',
|
||||
))
|
||||
);
|
||||
|
||||
return overlays;
|
||||
}
|
||||
|
||||
static Polygon _buildCone(
|
||||
LatLng origin,
|
||||
double bearingDeg,
|
||||
double zoom, {
|
||||
bool isPending = false,
|
||||
}) {
|
||||
final halfAngle = kDirectionConeHalfAngle;
|
||||
final length = kDirectionConeBaseLength * math.pow(2, 15 - zoom);
|
||||
|
||||
LatLng project(double deg) {
|
||||
final rad = deg * math.pi / 180;
|
||||
final dLat = length * math.cos(rad);
|
||||
final dLon =
|
||||
length * math.sin(rad) / math.cos(origin.latitude * math.pi / 180);
|
||||
return LatLng(origin.latitude + dLat, origin.longitude + dLon);
|
||||
}
|
||||
|
||||
final left = project(bearingDeg - halfAngle);
|
||||
final right = project(bearingDeg + halfAngle);
|
||||
|
||||
// Use purple color for pending uploads
|
||||
final color = isPending ? Colors.purple : Colors.redAccent;
|
||||
|
||||
return Polygon(
|
||||
points: [origin, left, right, origin],
|
||||
color: color.withOpacity(0.25),
|
||||
borderColor: color,
|
||||
borderStrokeWidth: 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
107
lib/widgets/map/map_overlays.dart
Normal file
107
lib/widgets/map/map_overlays.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
|
||||
import '../../app_state.dart';
|
||||
import '../../dev_config.dart';
|
||||
|
||||
/// Widget that renders all map overlay UI elements
|
||||
class MapOverlays extends StatelessWidget {
|
||||
final MapController mapController;
|
||||
final UploadMode uploadMode;
|
||||
final AddCameraSession? session;
|
||||
|
||||
const MapOverlays({
|
||||
super.key,
|
||||
required this.mapController,
|
||||
required this.uploadMode,
|
||||
this.session,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
// MODE INDICATOR badge (top-right)
|
||||
if (uploadMode == UploadMode.sandbox || uploadMode == UploadMode.simulate)
|
||||
Positioned(
|
||||
top: 18,
|
||||
right: 14,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: uploadMode == UploadMode.sandbox
|
||||
? Colors.orange.withOpacity(0.90)
|
||||
: Colors.deepPurple.withOpacity(0.80),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 5, offset: Offset(0,2)),
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
uploadMode == UploadMode.sandbox
|
||||
? 'SANDBOX MODE'
|
||||
: 'SIMULATE',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Zoom indicator, positioned above scale bar
|
||||
Positioned(
|
||||
left: 10,
|
||||
bottom: kZoomIndicatorBottomOffset,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.52),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final zoom = mapController.camera.zoom;
|
||||
return Text(
|
||||
'Zoom: ${zoom.toStringAsFixed(2)}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Attribution overlay
|
||||
Positioned(
|
||||
bottom: kAttributionBottomOffset,
|
||||
left: 10,
|
||||
child: Container(
|
||||
color: Colors.white70,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: const Text(
|
||||
'© OpenStreetMap and contributors',
|
||||
style: TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Fixed pin when adding camera
|
||||
if (session != null)
|
||||
IgnorePointer(
|
||||
child: Center(
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, kAddPinYOffset),
|
||||
child: const Icon(Icons.place, size: 40, color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -18,63 +10,12 @@ import '../services/map_data_provider.dart';
|
||||
import '../services/offline_area_service.dart';
|
||||
import '../models/osm_camera_node.dart';
|
||||
import 'debouncer.dart';
|
||||
import 'camera_tag_sheet.dart';
|
||||
import 'tile_provider_with_cache.dart';
|
||||
import 'camera_provider_with_cache.dart';
|
||||
import 'package:flock_map_app/dev_config.dart';
|
||||
|
||||
// --- Smart marker widget for camera with single/double tap distinction
|
||||
class _CameraMapMarker extends StatefulWidget {
|
||||
final OsmCameraNode node;
|
||||
final MapController mapController;
|
||||
const _CameraMapMarker({required this.node, required this.mapController, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_CameraMapMarker> createState() => _CameraMapMarkerState();
|
||||
}
|
||||
|
||||
class _CameraMapMarkerState extends State<_CameraMapMarker> {
|
||||
Timer? _tapTimer;
|
||||
// From dev_config.dart for build-time parameters
|
||||
static const Duration tapTimeout = kMarkerTapTimeout;
|
||||
|
||||
void _onTap() {
|
||||
_tapTimer = Timer(tapTimeout, () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (_) => CameraTagSheet(node: widget.node),
|
||||
showDragHandle: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _onDoubleTap() {
|
||||
_tapTimer?.cancel();
|
||||
widget.mapController.move(widget.node.coord, widget.mapController.camera.zoom + 1);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tapTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Check if this is a pending upload
|
||||
final isPending = widget.node.tags.containsKey('_pending_upload') &&
|
||||
widget.node.tags['_pending_upload'] == 'true';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: _onTap,
|
||||
onDoubleTap: _onDoubleTap,
|
||||
child: Icon(
|
||||
Icons.videocam,
|
||||
color: isPending ? Colors.purple : Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'map/camera_markers.dart';
|
||||
import 'map/direction_cones.dart';
|
||||
import 'map/map_overlays.dart';
|
||||
import '../dev_config.dart';
|
||||
|
||||
class MapView extends StatefulWidget {
|
||||
final MapController controller;
|
||||
@@ -229,39 +170,19 @@ class _MapViewState extends State<MapView> {
|
||||
final cameras = (mapBounds != null)
|
||||
? cameraProvider.getCachedCamerasForBounds(mapBounds)
|
||||
: <OsmCameraNode>[];
|
||||
final markers = <Marker>[
|
||||
...cameras
|
||||
.where((n) => n.coord.latitude != 0 || n.coord.longitude != 0)
|
||||
.where((n) => n.coord.latitude.abs() <= 90 && n.coord.longitude.abs() <= 180)
|
||||
.map((n) => Marker(
|
||||
point: n.coord,
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: _CameraMapMarker(node: n, mapController: _controller),
|
||||
)),
|
||||
if (_currentLatLng != null)
|
||||
Marker(
|
||||
point: _currentLatLng!,
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: const Icon(Icons.my_location, color: Colors.blue),
|
||||
),
|
||||
];
|
||||
|
||||
final markers = CameraMarkersBuilder.buildCameraMarkers(
|
||||
cameras: cameras,
|
||||
mapController: _controller,
|
||||
userLocation: _currentLatLng,
|
||||
);
|
||||
|
||||
final overlays = DirectionConesBuilder.buildDirectionCones(
|
||||
cameras: cameras,
|
||||
zoom: zoom,
|
||||
session: session,
|
||||
);
|
||||
|
||||
final overlays = <Polygon>[
|
||||
if (session != null && session.target != null)
|
||||
_buildCone(session.target!, session.directionDegrees, zoom),
|
||||
...cameras
|
||||
.where((n) => n.hasDirection && n.directionDeg != null)
|
||||
.where((n) => n.coord.latitude != 0 || n.coord.longitude != 0)
|
||||
.where((n) => n.coord.latitude.abs() <= 90 && n.coord.longitude.abs() <= 180)
|
||||
.map((n) => _buildCone(
|
||||
n.coord,
|
||||
n.directionDeg!,
|
||||
zoom,
|
||||
isPending: n.tags.containsKey('_pending_upload') && n.tags['_pending_upload'] == 'true',
|
||||
)),
|
||||
];
|
||||
return Stack(
|
||||
children: [
|
||||
PolygonLayer(polygons: overlays),
|
||||
@@ -332,113 +253,14 @@ class _MapViewState extends State<MapView> {
|
||||
],
|
||||
),
|
||||
|
||||
// MODE INDICATOR badge (top-right)
|
||||
if (appState.uploadMode == UploadMode.sandbox || appState.uploadMode == UploadMode.simulate)
|
||||
Positioned(
|
||||
top: 18,
|
||||
right: 14,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: appState.uploadMode == UploadMode.sandbox
|
||||
? Colors.orange.withOpacity(0.90)
|
||||
: Colors.deepPurple.withOpacity(0.80),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 5, offset: Offset(0,2)),
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
appState.uploadMode == UploadMode.sandbox
|
||||
? 'SANDBOX MODE'
|
||||
: 'SIMULATE',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Zoom indicator, positioned above scale bar
|
||||
Positioned(
|
||||
left: 10,
|
||||
bottom: kZoomIndicatorBottomOffset,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.52),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final zoom = _controller.camera.zoom;
|
||||
return Text(
|
||||
'Zoom: ${zoom.toStringAsFixed(2)}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// All map overlays (mode indicator, zoom, attribution, add pin)
|
||||
MapOverlays(
|
||||
mapController: _controller,
|
||||
uploadMode: appState.uploadMode,
|
||||
session: session,
|
||||
),
|
||||
// Attribution overlay
|
||||
Positioned(
|
||||
bottom: kAttributionBottomOffset,
|
||||
left: 10,
|
||||
child: Container(
|
||||
color: Colors.white70,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: const Text(
|
||||
'© OpenStreetMap and contributors',
|
||||
style: TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Fixed pin when adding camera
|
||||
if (session != null)
|
||||
IgnorePointer(
|
||||
child: Center(
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, kAddPinYOffset),
|
||||
child: Icon(Icons.place, size: 40, color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Polygon _buildCone(LatLng origin, double bearingDeg, double zoom, {bool isPending = false}) {
|
||||
final halfAngle = kDirectionConeHalfAngle;
|
||||
final length = kDirectionConeBaseLength * math.pow(2, 15 - zoom);
|
||||
|
||||
LatLng _project(double deg) {
|
||||
final rad = deg * math.pi / 180;
|
||||
final dLat = length * math.cos(rad);
|
||||
final dLon =
|
||||
length * math.sin(rad) / math.cos(origin.latitude * math.pi / 180);
|
||||
return LatLng(origin.latitude + dLat, origin.longitude + dLon);
|
||||
}
|
||||
|
||||
final left = _project(bearingDeg - halfAngle);
|
||||
final right = _project(bearingDeg + halfAngle);
|
||||
|
||||
// Use purple color for pending uploads
|
||||
final color = isPending ? Colors.purple : Colors.redAccent;
|
||||
|
||||
return Polygon(
|
||||
points: [origin, left, right, origin],
|
||||
color: color.withOpacity(0.25),
|
||||
borderColor: color,
|
||||
borderStrokeWidth: 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user