feat : Map Widget

This commit is contained in:
2025-08-03 14:17:08 +03:30
parent 7d3ab64705
commit 693d8cbfab
9 changed files with 232 additions and 289 deletions

View File

@@ -1,10 +0,0 @@
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
class CustomMarker {
final LatLng point;
final VoidCallback? onTap;
final int? id;
CustomMarker({ this.id, required this.point, this.onTap});
}

View File

@@ -1,170 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_animations/flutter_map_animations.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:rasadyar_core/utils/logger_utils.dart';
import 'custom_marker.dart';
enum ErrorLocationType { serviceDisabled, permissionDenied, none }
class MapWidgetLogic extends GetxController with GetTickerProviderStateMixin {
Rx<LatLng> currentLocation = LatLng(35.824891, 50.948025).obs;
String tileType = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
RxList<CustomMarker> markers = <CustomMarker>[].obs;
RxList<LatLng> allMarkers = <LatLng>[].obs;
Rx<MapController> mapController = MapController().obs;
RxList<ErrorLocationType> errorLocationType = RxList();
late final AnimatedMapController animatedMapController;
Timer? _debounceTimer;
RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
animatedMapController = AnimatedMapController(
vsync: this,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
cancelPreviousAnimations: true,
);
locationServiceEnabled().then((value) {
if (!value) {
errorLocationType.add(ErrorLocationType.serviceDisabled);
}
});
checkPermission().then((value) {
if (!value) {
errorLocationType.add(ErrorLocationType.permissionDenied);
}
});
listenToLocationServiceStatus().listen((event) {
if (!event) {
errorLocationType.add(ErrorLocationType.serviceDisabled);
} else {
errorLocationType.remove(ErrorLocationType.serviceDisabled);
}
});
}
@override
void onReady() {
super.onReady();
determineCurrentPosition();
}
@override
void onClose() {
super.onClose();
_debounceTimer?.cancel();
animatedMapController.dispose();
mapController.close();
}
Stream<bool> listenToLocationServiceStatus() {
return Geolocator.getServiceStatusStream().map((status) {
return status == ServiceStatus.enabled;
});
}
Future<bool> locationServiceEnabled() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return false;
}
return true;
}
Future<bool> checkPermission({bool request = false}) async {
try {
final LocationPermission permission = await Geolocator.checkPermission();
switch (permission) {
case LocationPermission.denied:
final LocationPermission requestResult =
await Geolocator.requestPermission();
return requestResult != LocationPermission.denied &&
requestResult != LocationPermission.deniedForever;
case LocationPermission.deniedForever:
return request ? await Geolocator.openAppSettings() : false;
case LocationPermission.always:
case LocationPermission.whileInUse:
return true;
default:
return false;
}
} catch (e) {
eLog(e);
return await Geolocator.openLocationSettings();
}
}
Future<void> determineCurrentPosition() async {
final position = await Geolocator.getCurrentPosition(
locationSettings: AndroidSettings(accuracy: LocationAccuracy.best),
);
final latLng = LatLng(position.latitude, position.longitude);
currentLocation.value = latLng;
markers.add(
CustomMarker(id: -1, point: latLng, ),
);
animatedMapController.animateTo(
dest: latLng,
zoom: 18,
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 1500),
);
}
void debouncedUpdateVisibleMarkers({required LatLng center}) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
final filtered = filterNearbyMarkers({
'markers': allMarkers,
'centerLat': center.latitude,
'centerLng': center.longitude,
'radius': 1000.0,
});
// markers.addAll(filtered);
});
}
List<LatLng> filterNearbyMarkers(Map<String, dynamic> args) {
final List<LatLng> rawMarkers = args['markers'];
final double centerLat = args['centerLat'];
final double centerLng = args['centerLng'];
final double radiusInMeters = args['radius'];
final center = LatLng(centerLat, centerLng);
final distance = Distance();
return rawMarkers
.where((marker) => distance(center, marker) <= radiusInMeters)
.toList();
}
void addMarker(CustomMarker marker) {
markers.add(marker);
}
void setMarkers(List<CustomMarker> newMarkers) {
markers.value = newMarkers;
}
void clearMarkers() {
markers.clear();
}
}

View File

@@ -1,207 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:rasadyar_core/presentation/common/app_color.dart';
import 'package:rasadyar_core/presentation/common/app_fonts.dart';
import 'package:rasadyar_core/presentation/common/assets.gen.dart';
import 'package:rasadyar_core/presentation/widget/buttons/elevated.dart';
import 'package:rasadyar_core/presentation/widget/buttons/fab.dart';
import 'package:rasadyar_core/presentation/widget/buttons/outline_elevated.dart';
import 'logic.dart';
class MapWidget extends GetView<MapWidgetLogic> {
final VoidCallback? initOnTap;
final Widget? initMarkerWidget;
final Widget markerWidget;
const MapWidget({
this.initOnTap,
this.initMarkerWidget,
required this.markerWidget,
super.key,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
ObxValue((errorType) {
if (errorType.isNotEmpty) {
if (errorType.contains(ErrorLocationType.serviceDisabled)) {
Future.microtask(() {
Get.defaultDialog(
title: 'خطا',
content: const Text('سرویس مکان‌یابی غیرفعال است'),
cancel: ROutlinedElevated(
text: 'بررسی مجدد',
width: 120,
textStyle: AppFonts.yekan16,
onPressed: () async {
var service = await controller.locationServiceEnabled();
if (service) {
controller.errorLocationType.remove(
ErrorLocationType.serviceDisabled,
);
Get.back();
}
// Don't call Get.back() if service is still disabled
},
),
confirm: RElevated(
text: 'روشن کردن',
textStyle: AppFonts.yekan16,
width: 120,
onPressed: () async {
var res = await Geolocator.openLocationSettings();
if (res) {
var service = await controller.locationServiceEnabled();
if (service) {
controller.errorLocationType.remove(
ErrorLocationType.serviceDisabled,
);
Get.back();
}
}
},
),
contentPadding: EdgeInsets.all(8),
onWillPop: () async {
return controller.errorLocationType.isEmpty;
},
barrierDismissible: false,
);
});
} else {
Future.microtask(() {
Get.defaultDialog(
title: 'خطا',
content: const Text(' دسترسی به سرویس مکان‌یابی غیرفعال است'),
cancel: ROutlinedElevated(
text: 'بررسی مجدد',
width: 120,
textStyle: AppFonts.yekan16,
onPressed: () async {
await controller.checkPermission();
},
),
confirm: RElevated(
text: 'اجازه دادن',
textStyle: AppFonts.yekan16,
width: 120,
onPressed: () async {
var res = await controller.checkPermission(request: true);
if (res) {
controller.errorLocationType.remove(
ErrorLocationType.permissionDenied,
);
Get.back();
}
},
),
contentPadding: EdgeInsets.all(8),
onWillPop: () async {
return controller.errorLocationType.isEmpty;
},
barrierDismissible: false,
);
});
}
}
return const SizedBox.shrink();
}, controller.errorLocationType),
_buildMap(),
_buildGpsButton(),
_buildFilterButton(),
],
);
}
Widget _buildMap() {
return ObxValue((currentLocation) {
return FlutterMap(
mapController: controller.animatedMapController.mapController,
options: MapOptions(
initialCenter: currentLocation.value,
initialZoom: 18,
onPositionChanged: (camera, hasGesture) {
if (hasGesture) {
controller.debouncedUpdateVisibleMarkers(center: camera.center);
}
//controller.debouncedUpdateVisibleMarkers(center: camera.center);
},
),
children: [
TileLayer(urlTemplate: controller.tileType),
ObxValue((markers) {
return MarkerLayer(
markers:
markers
.map(
(e) => Marker(
point: e.point,
child: GestureDetector(
onTap: e.id != -1 ? e.onTap : initOnTap,
child:
e.id != -1
? markerWidget
: initMarkerWidget ?? SizedBox.shrink(),
),
),
)
.toList(),
);
}, controller.markers),
],
);
}, controller.currentLocation);
}
Widget _buildGpsButton() {
return Positioned(
right: 10,
bottom: 83,
child: ObxValue((data) {
return RFab.small(
backgroundColor: AppColor.greenNormal,
isLoading: data.value,
icon: Assets.vec.gpsSvg.svg(),
onPressed: () async {
controller.isLoading.value = true;
await controller.determineCurrentPosition();
controller.isLoading.value = false;
},
);
}, controller.isLoading),
);
}
Widget _buildFilterButton() {
return Positioned(
right: 10,
bottom: 30,
child: RFab.small(
backgroundColor: AppColor.blueNormal,
icon: Assets.vec.filterSvg.svg(width: 24, height: 24),
onPressed: () {},
),
);
}
/*Marker markerWidget({required LatLng marker, required VoidCallback onTap}) {
return Marker(
point: marker,
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 36,
height: 36,
child: Assets.vec.mapMarkerSvg.svg(width: 30, height: 30),
),
),
);
}*/
}