2026-04-05-maplibra succsess for all and add navigation paage
This commit is contained in:
632
lib/views/home/navigation/navigation_controller.dart
Normal file
632
lib/views/home/navigation/navigation_controller.dart
Normal file
@@ -0,0 +1,632 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:Intaleq/views/widgets/error_snakbar.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart'; // Replaced Google Maps
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/country_polygons.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../controller/functions/tts.dart';
|
||||
import '../../../controller/home/decode_polyline_isolate.dart';
|
||||
import '../../../env/env.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
|
||||
class NavigationController extends GetxController {
|
||||
bool isLoading = false;
|
||||
MaplibreMapController? mapController;
|
||||
bool isStyleLoaded = false;
|
||||
final TextEditingController placeDestinationController =
|
||||
TextEditingController();
|
||||
|
||||
LatLng? myLocation;
|
||||
double heading = 0.0;
|
||||
|
||||
// MapLibre Object Tracking
|
||||
Symbol? carSymbol;
|
||||
Symbol? destinationSymbol;
|
||||
Line? remainingRouteLine;
|
||||
Line? traveledRouteLine;
|
||||
|
||||
Timer? _locationUpdateTimer;
|
||||
final Duration _currentUpdateInterval = const Duration(seconds: 1);
|
||||
LatLng? _lastRecordedLocation;
|
||||
|
||||
List<dynamic> placesDestination = [];
|
||||
Timer? _debounce;
|
||||
|
||||
LatLng? _finalDestination;
|
||||
List<Map<String, dynamic>> routeSteps = [];
|
||||
List<LatLng> _fullRouteCoordinates = [];
|
||||
int _lastTraveledIndexInFullRoute = 0;
|
||||
|
||||
bool _nextInstructionSpoken = false;
|
||||
String currentInstruction = "";
|
||||
String nextInstruction = "";
|
||||
int currentStepIndex = 0;
|
||||
|
||||
double currentSpeed = 0.0;
|
||||
String distanceToNextStep = "";
|
||||
|
||||
static final String _routeApiBaseUrl =
|
||||
"${AppLink.routesOsm}/route/v1/driving";
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
await _getCurrentLocationAndStartUpdates();
|
||||
if (!Get.isRegistered<TextToSpeechController>()) {
|
||||
Get.put(TextToSpeechController());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_locationUpdateTimer?.cancel();
|
||||
mapController?.dispose();
|
||||
_debounce?.cancel();
|
||||
placeDestinationController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Map Initialization & Callbacks
|
||||
// =======================================================================
|
||||
|
||||
void onMapCreated(MaplibreMapController controller) {
|
||||
mapController = controller;
|
||||
}
|
||||
|
||||
Future<void> onStyleLoaded() async {
|
||||
isStyleLoaded = true;
|
||||
await _loadCustomIcons();
|
||||
|
||||
if (myLocation != null) {
|
||||
animateCameraToPosition(myLocation!);
|
||||
_updateCarMarker();
|
||||
}
|
||||
|
||||
if (_fullRouteCoordinates.isNotEmpty) {
|
||||
_updatePolylinesSets([], _fullRouteCoordinates);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onMapLongPressed(Point<double> point, LatLng tappedPoint) async {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: const Text('بدء الملاحة؟'),
|
||||
content: const Text('هل تريد الذهاب إلى هذا الموقع المحدد؟'),
|
||||
actionsAlignment: MainAxisAlignment.spaceBetween,
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('إلغاء', style: TextStyle(color: Colors.grey)),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('اذهب الآن'),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Location Management
|
||||
// =======================================================================
|
||||
|
||||
Future<void> _getCurrentLocationAndStartUpdates() async {
|
||||
try {
|
||||
Position position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
myLocation = LatLng(position.latitude, position.longitude);
|
||||
update();
|
||||
if (isStyleLoaded) animateCameraToPosition(myLocation!);
|
||||
_startLocationTimer();
|
||||
} catch (e) {
|
||||
Log.print("Error getting initial location: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _startLocationTimer() {
|
||||
_locationUpdateTimer?.cancel();
|
||||
_locationUpdateTimer = Timer.periodic(_currentUpdateInterval, (timer) {
|
||||
_updateLocationAndProcess();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updateLocationAndProcess() async {
|
||||
try {
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
final newLoc = LatLng(position.latitude, position.longitude);
|
||||
|
||||
if (_lastRecordedLocation != null) {
|
||||
double dist = Geolocator.distanceBetween(
|
||||
newLoc.latitude,
|
||||
newLoc.longitude,
|
||||
_lastRecordedLocation!.latitude,
|
||||
_lastRecordedLocation!.longitude);
|
||||
if (dist < 2.0) return;
|
||||
}
|
||||
|
||||
myLocation = newLoc;
|
||||
_lastRecordedLocation = newLoc;
|
||||
heading = position.heading;
|
||||
currentSpeed = position.speed * 3.6;
|
||||
|
||||
if (isStyleLoaded) _updateCarMarker();
|
||||
|
||||
if (_fullRouteCoordinates.isNotEmpty) {
|
||||
animateCameraToPosition(myLocation!, bearing: heading, zoom: 18.0);
|
||||
_updateTraveledPolylineSmart(myLocation!);
|
||||
_checkNavigationStep(myLocation!);
|
||||
}
|
||||
|
||||
update();
|
||||
} catch (e) {
|
||||
// Log.print("Loc update error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateCarMarker() async {
|
||||
if (myLocation == null || mapController == null || !isStyleLoaded) return;
|
||||
|
||||
if (carSymbol == null) {
|
||||
carSymbol = await mapController!.addSymbol(SymbolOptions(
|
||||
geometry: myLocation,
|
||||
iconImage: 'car_icon',
|
||||
iconSize: 1.0,
|
||||
iconRotate: heading,
|
||||
));
|
||||
} else {
|
||||
mapController!.updateSymbol(
|
||||
carSymbol!,
|
||||
SymbolOptions(
|
||||
geometry: myLocation,
|
||||
iconRotate: heading,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void animateCameraToPosition(LatLng position,
|
||||
{double zoom = 17.0, double bearing = 0.0}) {
|
||||
mapController?.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: position, zoom: zoom, bearing: bearing, tilt: 45.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Route Management
|
||||
// =======================================================================
|
||||
|
||||
void _updateTraveledPolylineSmart(LatLng currentPos) {
|
||||
if (_fullRouteCoordinates.isEmpty) return;
|
||||
|
||||
int searchWindow = 60;
|
||||
int startIndex = _lastTraveledIndexInFullRoute;
|
||||
int endIndex = min(startIndex + searchWindow, _fullRouteCoordinates.length);
|
||||
|
||||
double minDistance = double.infinity;
|
||||
int closestIndex = startIndex;
|
||||
bool foundCloser = false;
|
||||
|
||||
for (int i = startIndex; i < endIndex; i++) {
|
||||
final point = _fullRouteCoordinates[i];
|
||||
final dist = Geolocator.distanceBetween(currentPos.latitude,
|
||||
currentPos.longitude, point.latitude, point.longitude);
|
||||
|
||||
if (dist < minDistance) {
|
||||
minDistance = dist;
|
||||
closestIndex = i;
|
||||
foundCloser = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundCloser &&
|
||||
minDistance < 50 &&
|
||||
closestIndex > _lastTraveledIndexInFullRoute) {
|
||||
_lastTraveledIndexInFullRoute = closestIndex;
|
||||
final remaining =
|
||||
_fullRouteCoordinates.sublist(_lastTraveledIndexInFullRoute);
|
||||
final traveled =
|
||||
_fullRouteCoordinates.sublist(0, _lastTraveledIndexInFullRoute + 1);
|
||||
_updatePolylinesSets(traveled, remaining);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updatePolylinesSets(
|
||||
List<LatLng> traveled, List<LatLng> remaining) async {
|
||||
if (mapController == null || !isStyleLoaded) return;
|
||||
|
||||
if (remainingRouteLine != null)
|
||||
await mapController!.removeLine(remainingRouteLine!);
|
||||
if (traveledRouteLine != null)
|
||||
await mapController!.removeLine(traveledRouteLine!);
|
||||
|
||||
if (remaining.isNotEmpty) {
|
||||
remainingRouteLine = await mapController!.addLine(LineOptions(
|
||||
geometry: remaining,
|
||||
lineColor: '#0D47A1',
|
||||
lineWidth: 6.0,
|
||||
lineJoin: 'round',
|
||||
));
|
||||
}
|
||||
|
||||
if (traveled.isNotEmpty) {
|
||||
traveledRouteLine = await mapController!.addLine(LineOptions(
|
||||
geometry: traveled,
|
||||
lineColor: '#BDBDBD',
|
||||
lineWidth: 6.0,
|
||||
lineJoin: 'round',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Routing API & Navigation
|
||||
// =======================================================================
|
||||
|
||||
Future<void> getRoute(LatLng origin, LatLng destination) async {
|
||||
String coords =
|
||||
"${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}";
|
||||
String url =
|
||||
"$_routeApiBaseUrl/$coords?steps=true&overview=full&geometries=polyline";
|
||||
|
||||
try {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode != 200) {
|
||||
mySnackbarWarning('تعذر الاتصال بخدمة التوجيه.');
|
||||
return;
|
||||
}
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
if (responseData['code'] != 'Ok' ||
|
||||
(responseData['routes'] as List).isEmpty) {
|
||||
mySnackbarWarning('لم يتم العثور على مسار.');
|
||||
return;
|
||||
}
|
||||
|
||||
var route = responseData['routes'][0];
|
||||
final pointsString = route['geometry'];
|
||||
// فك تشفير Polyline بطريقة آمنة نوعياً (Type-Safe)
|
||||
_fullRouteCoordinates = await compute<String, List<LatLng>>(
|
||||
decodePolylineIsolate, pointsString.toString());
|
||||
|
||||
_lastTraveledIndexInFullRoute = 0;
|
||||
if (isStyleLoaded) _updatePolylinesSets([], _fullRouteCoordinates);
|
||||
|
||||
var legs = route['legs'] as List;
|
||||
if (legs.isNotEmpty) {
|
||||
var steps = legs[0]['steps'] as List;
|
||||
routeSteps = List<Map<String, dynamic>>.from(steps);
|
||||
} else {
|
||||
routeSteps = [];
|
||||
}
|
||||
|
||||
for (var step in routeSteps) {
|
||||
step['instruction_text'] = _createInstructionFromManeuver(step);
|
||||
}
|
||||
|
||||
currentStepIndex = 0;
|
||||
_nextInstructionSpoken = false;
|
||||
if (routeSteps.isNotEmpty) {
|
||||
currentInstruction = routeSteps[0]['instruction_text'];
|
||||
nextInstruction = routeSteps.length > 1
|
||||
? "ثم ${routeSteps[1]['instruction_text']}"
|
||||
: "الوجهة النهائية";
|
||||
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
||||
}
|
||||
|
||||
if (_fullRouteCoordinates.isNotEmpty) {
|
||||
final bounds = _boundsFromLatLngList(_fullRouteCoordinates);
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngBounds(bounds,
|
||||
bottom: 200, top: 150, left: 50, right: 50));
|
||||
}
|
||||
|
||||
update();
|
||||
} catch (e) {
|
||||
Log.print("GetRoute Error: $e");
|
||||
Get.snackbar('خطأ', 'حدث خطأ غير متوقع.');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Map Object Handlers ---
|
||||
|
||||
Future<void> startNavigationTo(LatLng destination,
|
||||
{String infoWindowTitle = ''}) async {
|
||||
isLoading = true;
|
||||
update();
|
||||
try {
|
||||
_finalDestination = destination;
|
||||
await clearRoute(isNewRoute: true);
|
||||
|
||||
if (isStyleLoaded && mapController != null) {
|
||||
destinationSymbol = await mapController!.addSymbol(SymbolOptions(
|
||||
geometry: destination,
|
||||
iconImage: 'dest_icon',
|
||||
iconSize: 1.0,
|
||||
textField: infoWindowTitle,
|
||||
textOffset: const Offset(0, 2),
|
||||
));
|
||||
}
|
||||
|
||||
if (myLocation != null) await getRoute(myLocation!, destination);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> recalculateRoute() async {
|
||||
if (myLocation == null || _finalDestination == null || isLoading) return;
|
||||
isLoading = true;
|
||||
update();
|
||||
Get.snackbar('إعادة التوجيه', 'جاري حساب مسار جديد...',
|
||||
backgroundColor: AppColor.goldenBronze);
|
||||
await getRoute(myLocation!, _finalDestination!);
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> clearRoute({bool isNewRoute = false}) async {
|
||||
if (!isNewRoute) {
|
||||
if (destinationSymbol != null && mapController != null) {
|
||||
await mapController!.removeSymbol(destinationSymbol!);
|
||||
destinationSymbol = null;
|
||||
}
|
||||
if (remainingRouteLine != null && mapController != null) {
|
||||
await mapController!.removeLine(remainingRouteLine!);
|
||||
remainingRouteLine = null;
|
||||
}
|
||||
if (traveledRouteLine != null && mapController != null) {
|
||||
await mapController!.removeLine(traveledRouteLine!);
|
||||
traveledRouteLine = null;
|
||||
}
|
||||
_finalDestination = null;
|
||||
}
|
||||
routeSteps.clear();
|
||||
_fullRouteCoordinates.clear();
|
||||
_lastTraveledIndexInFullRoute = 0;
|
||||
currentInstruction = "";
|
||||
nextInstruction = "";
|
||||
distanceToNextStep = "";
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> _loadCustomIcons() async {
|
||||
if (mapController == null) return;
|
||||
|
||||
final ByteData carBytes = await rootBundle.load('assets/images/car.png');
|
||||
final Uint8List carList = carBytes.buffer.asUint8List();
|
||||
await mapController!.addImage('car_icon', carList);
|
||||
|
||||
final ByteData destBytes = await rootBundle.load('assets/images/b.png');
|
||||
final Uint8List destList = destBytes.buffer.asUint8List();
|
||||
await mapController!.addImage('dest_icon', destList);
|
||||
}
|
||||
|
||||
// --- Step Tracking & Instructions (Omitted unchanged logic to save space, retain your existing string matchers) ---
|
||||
void _checkNavigationStep(LatLng currentPosition) {
|
||||
if (routeSteps.isEmpty || currentStepIndex >= routeSteps.length) return;
|
||||
final step = routeSteps[currentStepIndex];
|
||||
final maneuver = step['maneuver'];
|
||||
final List<dynamic> location = maneuver['location'];
|
||||
final endLatLng = LatLng(location[1], location[0]);
|
||||
|
||||
final distance = Geolocator.distanceBetween(
|
||||
currentPosition.latitude,
|
||||
currentPosition.longitude,
|
||||
endLatLng.latitude,
|
||||
endLatLng.longitude,
|
||||
);
|
||||
|
||||
distanceToNextStep = distance > 1000
|
||||
? "${(distance / 1000).toStringAsFixed(1)} كم"
|
||||
: "${distance.toStringAsFixed(0)} متر";
|
||||
|
||||
if (distance < 50 &&
|
||||
!_nextInstructionSpoken &&
|
||||
nextInstruction.isNotEmpty) {
|
||||
Get.find<TextToSpeechController>().speakText(nextInstruction);
|
||||
_nextInstructionSpoken = true;
|
||||
}
|
||||
|
||||
if (distance < 20) _advanceStep();
|
||||
}
|
||||
|
||||
void _advanceStep() {
|
||||
currentStepIndex++;
|
||||
if (currentStepIndex < routeSteps.length) {
|
||||
currentInstruction = routeSteps[currentStepIndex]['instruction_text'];
|
||||
nextInstruction = (currentStepIndex + 1) < routeSteps.length
|
||||
? "ثم ${routeSteps[currentStepIndex + 1]['instruction_text']}"
|
||||
: "ستصل إلى وجهتك";
|
||||
_nextInstructionSpoken = false;
|
||||
update();
|
||||
} else {
|
||||
_finishNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
void _finishNavigation() {
|
||||
currentInstruction = "لقد وصلت إلى وجهتك";
|
||||
nextInstruction = "";
|
||||
distanceToNextStep = "";
|
||||
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
||||
update();
|
||||
}
|
||||
|
||||
String _createInstructionFromManeuver(Map<String, dynamic> step) {
|
||||
if (step['maneuver'] == null) return "تابع المسير";
|
||||
final maneuver = step['maneuver'];
|
||||
final type = maneuver['type'] ?? 'continue';
|
||||
final modifier = maneuver['modifier'] ?? 'straight';
|
||||
final name = step['name'] ?? '';
|
||||
String instruction = "";
|
||||
|
||||
switch (type) {
|
||||
case 'depart':
|
||||
instruction = "انطلق";
|
||||
break;
|
||||
case 'arrive':
|
||||
return "لقد وصلت إلى وجهتك، $name";
|
||||
case 'turn':
|
||||
case 'fork':
|
||||
case 'roundabout':
|
||||
case 'merge':
|
||||
case 'on ramp':
|
||||
case 'off ramp':
|
||||
case 'end of road':
|
||||
instruction = _getTurnInstruction(modifier);
|
||||
break;
|
||||
case 'new name':
|
||||
instruction = "تابع المسير";
|
||||
break;
|
||||
default:
|
||||
instruction = "تابع المسير";
|
||||
}
|
||||
|
||||
if (name.isNotEmpty)
|
||||
instruction += (type == 'new name' || type == 'continue')
|
||||
? " على $name"
|
||||
: " نحو $name";
|
||||
return instruction;
|
||||
}
|
||||
|
||||
String _getTurnInstruction(String modifier) {
|
||||
switch (modifier) {
|
||||
case 'uturn':
|
||||
return "قم بالاستدارة والعودة";
|
||||
case 'sharp right':
|
||||
return "انعطف يميناً بحدة";
|
||||
case 'right':
|
||||
return "انعطف يميناً";
|
||||
case 'slight right':
|
||||
return "انعطف يميناً قليلاً";
|
||||
case 'straight':
|
||||
return "استمر للأمام";
|
||||
case 'slight left':
|
||||
return "انعطف يساراً قليلاً";
|
||||
case 'left':
|
||||
return "انعطف يساراً";
|
||||
case 'sharp left':
|
||||
return "انعطف يساراً بحدة";
|
||||
default:
|
||||
return "اتجه";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Search & Utils (Retained entirely, no map logic here) ---
|
||||
Future<void> getPlaces() async {
|
||||
final q = placeDestinationController.text.trim();
|
||||
if (q.length < 3) {
|
||||
placesDestination = [];
|
||||
update();
|
||||
return;
|
||||
}
|
||||
if (myLocation == null) return;
|
||||
|
||||
final lat = myLocation!.latitude;
|
||||
final lng = myLocation!.longitude;
|
||||
const radiusKm = 200.0;
|
||||
final payload = {
|
||||
'query': q,
|
||||
'lat_min': (lat - _kmToLatDelta(radiusKm)).toString(),
|
||||
'lat_max': (lat + _kmToLatDelta(radiusKm)).toString(),
|
||||
'lng_min': (lng - _kmToLngDelta(radiusKm, lat)).toString(),
|
||||
'lng_max': (lng + _kmToLngDelta(radiusKm, lat)).toString(),
|
||||
};
|
||||
|
||||
try {
|
||||
final response =
|
||||
await CRUD().post(link: AppLink.getPlacesSyria, payload: payload);
|
||||
List list;
|
||||
if (response is Map && response['status'] == 'success')
|
||||
list = List.from(response['message'] as List);
|
||||
else if (response is List)
|
||||
list = List.from(response);
|
||||
else
|
||||
return;
|
||||
|
||||
for (final p in list) {
|
||||
final plat = double.tryParse(p['latitude']?.toString() ?? '0.0') ?? 0.0;
|
||||
final plng =
|
||||
double.tryParse(p['longitude']?.toString() ?? '0.0') ?? 0.0;
|
||||
p['distanceKm'] = _haversineKm(lat, lng, plat, plng);
|
||||
}
|
||||
|
||||
list.sort((a, b) =>
|
||||
(a['distanceKm'] as double).compareTo(b['distanceKm'] as double));
|
||||
placesDestination = list;
|
||||
update();
|
||||
} catch (e) {
|
||||
print('Exception in getPlaces: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> selectDestination(dynamic place) async {
|
||||
placeDestinationController.clear();
|
||||
placesDestination = [];
|
||||
final double lat = double.parse(place['latitude'].toString());
|
||||
final double lng = double.parse(place['longitude'].toString());
|
||||
await startNavigationTo(LatLng(lat, lng),
|
||||
infoWindowTitle: place['name'] ?? 'وجهة');
|
||||
}
|
||||
|
||||
void onSearchChanged(String query) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 700), () => getPlaces());
|
||||
}
|
||||
|
||||
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
const R = 6371.0;
|
||||
final dLat = (lat2 - lat1) * (pi / 180.0);
|
||||
final dLon = (lon2 - lon1) * (pi / 180.0);
|
||||
final a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(lat1 * pi / 180) *
|
||||
cos(lat2 * pi / 180) *
|
||||
sin(dLon / 2) *
|
||||
sin(dLon / 2);
|
||||
return R * 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||
}
|
||||
|
||||
double _kmToLatDelta(double km) => km / 111.32;
|
||||
double _kmToLngDelta(double km, double lat) =>
|
||||
km / (111.32 * cos(lat * pi / 180));
|
||||
|
||||
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
|
||||
double? x0, x1, y0, y1;
|
||||
for (LatLng latLng in list) {
|
||||
if (x0 == null) {
|
||||
x0 = x1 = latLng.latitude;
|
||||
y0 = y1 = latLng.longitude;
|
||||
} else {
|
||||
if (latLng.latitude > x1!) x1 = latLng.latitude;
|
||||
if (latLng.latitude < x0) x0 = latLng.latitude;
|
||||
if (latLng.longitude > y1!) y1 = latLng.longitude;
|
||||
if (latLng.longitude < y0!) y0 = latLng.longitude;
|
||||
}
|
||||
}
|
||||
return LatLngBounds(
|
||||
northeast: LatLng(x1!, y1!), southwest: LatLng(x0!, y0!));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user