Files
deflock-app/lib/widgets/map/marker_layer_builder.dart

247 lines
8.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_animations/flutter_map_animations.dart';
import 'package:latlong2/latlong.dart';
import '../../models/osm_node.dart';
import '../../models/suspected_location.dart';
import '../../app_state.dart';
import '../../dev_config.dart';
import '../camera_icon.dart';
import '../provisional_pin.dart';
import 'node_markers.dart';
import 'suspected_location_markers.dart';
/// Enumeration for different pin types in navigation
enum PinType { start, end }
/// Simple location pin widget for route visualization
class LocationPin extends StatelessWidget {
final PinType type;
const LocationPin({super.key, required this.type});
@override
Widget build(BuildContext context) {
return Container(
width: 32.0,
height: 32.0,
decoration: BoxDecoration(
color: type == PinType.start ? Colors.green : Colors.red,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: Icon(
type == PinType.start ? Icons.play_arrow : Icons.stop,
color: Colors.white,
size: 16,
),
);
}
}
/// Builds all marker layers for the map including surveillance nodes, suspected locations,
/// session markers, navigation pins, and route visualization.
class MarkerLayerBuilder {
/// Build complete marker layers for the map
static Widget buildMarkerLayers({
required List<OsmNode> nodesToRender,
required AnimatedMapController mapController,
required AppState appState,
required AddNodeSession? session,
required EditNodeSession? editSession,
required int? selectedNodeId,
required LatLng? userLocation,
required double currentZoom,
required LatLngBounds? mapBounds,
required Function(OsmNode)? onNodeTap,
required Function(SuspectedLocation)? onSuspectedLocationTap,
}) {
return LayoutBuilder(
builder: (context, constraints) {
// Determine if nodes should be dimmed and/or disabled
final shouldDimNodes = appState.selectedSuspectedLocation != null ||
appState.isInSearchMode ||
appState.showingOverview;
// Disable node interactions when navigation is in conflicting state
final shouldDisableNodeTaps = appState.isInSearchMode || appState.showingOverview;
final markers = NodeMarkersBuilder.buildNodeMarkers(
nodes: nodesToRender,
mapController: mapController.mapController,
userLocation: userLocation,
selectedNodeId: selectedNodeId,
onNodeTap: onNodeTap, // Keep the original callback
shouldDim: shouldDimNodes,
enabled: !shouldDisableNodeTaps, // Use enabled parameter instead
);
// Build suspected location markers (respect same zoom and count limits as nodes)
final suspectedLocationMarkers = <Marker>[];
if (appState.suspectedLocationsEnabled && mapBounds != null &&
currentZoom >= (appState.uploadMode == UploadMode.sandbox ? kOsmApiMinZoomLevel : kNodeMinZoomLevel)) {
final suspectedLocations = appState.getSuspectedLocationsInBoundsSync(
north: mapBounds.north,
south: mapBounds.south,
east: mapBounds.east,
west: mapBounds.west,
);
// Apply same node count limit as surveillance nodes
final maxNodes = appState.maxNodes;
final limitedSuspectedLocations = suspectedLocations.take(maxNodes).toList();
// Filter out suspected locations that are too close to real nodes
final filteredSuspectedLocations = _filterSuspectedLocationsByProximity(
suspectedLocations: limitedSuspectedLocations,
realNodes: nodesToRender,
minDistance: appState.suspectedLocationMinDistance,
);
suspectedLocationMarkers.addAll(
SuspectedLocationMarkersBuilder.buildSuspectedLocationMarkers(
locations: filteredSuspectedLocations,
mapController: mapController.mapController,
selectedLocationId: appState.selectedSuspectedLocation?.ticketNo,
onLocationTap: onSuspectedLocationTap, // Keep the original callback
shouldDimAll: shouldDisableNodeTaps,
enabled: !shouldDisableNodeTaps, // Use enabled parameter instead
),
);
}
// Build center marker for add/edit sessions
final centerMarkers = _buildSessionMarkers(
mapController: mapController,
session: session,
editSession: editSession,
);
// Build provisional pin for navigation/search mode
final navigationMarkers = _buildNavigationMarkers(appState);
// Build start/end pins for route visualization
final routeMarkers = _buildRouteMarkers(appState);
return MarkerLayer(
markers: [
...suspectedLocationMarkers,
...markers,
...centerMarkers,
...navigationMarkers,
...routeMarkers,
]
);
},
);
}
/// Build center markers for add/edit sessions
static List<Marker> _buildSessionMarkers({
required AnimatedMapController mapController,
required AddNodeSession? session,
required EditNodeSession? editSession,
}) {
final centerMarkers = <Marker>[];
if (session != null || editSession != null) {
try {
final center = mapController.mapController.camera.center;
centerMarkers.add(
Marker(
point: center,
width: kNodeIconDiameter,
height: kNodeIconDiameter,
child: CameraIcon(
type: editSession != null ? CameraIconType.editing : CameraIconType.mock,
),
),
);
} catch (_) {
// Controller not ready yet
}
}
return centerMarkers;
}
/// Build provisional pin for navigation/search mode
static List<Marker> _buildNavigationMarkers(AppState appState) {
final markers = <Marker>[];
if (appState.showProvisionalPin && appState.provisionalPinLocation != null) {
markers.add(
Marker(
point: appState.provisionalPinLocation!,
width: 32.0,
height: 32.0,
child: const ProvisionalPin(),
),
);
}
return markers;
}
/// Build start/end pins for route visualization
static List<Marker> _buildRouteMarkers(AppState appState) {
final markers = <Marker>[];
if (appState.showingOverview || appState.isInRouteMode || appState.isSettingSecondPoint) {
if (appState.routeStart != null) {
markers.add(
Marker(
point: appState.routeStart!,
width: 32.0,
height: 32.0,
child: const LocationPin(type: PinType.start),
),
);
}
if (appState.routeEnd != null) {
markers.add(
Marker(
point: appState.routeEnd!,
width: 32.0,
height: 32.0,
child: const LocationPin(type: PinType.end),
),
);
}
}
return markers;
}
/// Filter suspected locations that are too close to real nodes
static List<SuspectedLocation> _filterSuspectedLocationsByProximity({
required List<SuspectedLocation> suspectedLocations,
required List<OsmNode> realNodes,
required int minDistance, // in meters
}) {
if (minDistance <= 0) return suspectedLocations;
const distance = Distance();
final filteredLocations = <SuspectedLocation>[];
for (final suspected in suspectedLocations) {
bool tooClose = false;
for (final realNode in realNodes) {
final distanceMeters = distance.as(
LengthUnit.Meter,
suspected.centroid,
realNode.coord,
);
if (distanceMeters < minDistance) {
tooClose = true;
break;
}
}
if (!tooClose) {
filteredLocations.add(suspected);
}
}
return filteredLocations;
}
}