diff --git a/assets/json/mapstyle_dark.json b/assets/json/mapstyle_dark.json new file mode 100644 index 0000000..31c1819 --- /dev/null +++ b/assets/json/mapstyle_dark.json @@ -0,0 +1,191 @@ +[ + { + "elementType": "geometry", + "stylers": [ + { + "color": "#212121" + } + ] + }, + { + "elementType": "labels.icon", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#757575" + } + ] + }, + { + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#212121" + } + ] + }, + { + "featureType": "administrative", + "elementType": "geometry", + "stylers": [ + { + "color": "#757575" + } + ] + }, + { + "featureType": "administrative.country", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#9e9e9e" + } + ] + }, + { + "featureType": "administrative.land_parcel", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "administrative.locality", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#bdbdbd" + } + ] + }, + { + "featureType": "landscape.man_made", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "poi", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "poi", + "elementType": "labels.text.stroke", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "poi.attraction", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "poi.business", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry.fill", + "stylers": [ + { + "color": "#2c2c2c" + } + ] + }, + { + "featureType": "road", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#8a8a8a" + } + ] + }, + { + "featureType": "road.arterial", + "elementType": "geometry", + "stylers": [ + { + "color": "#373737" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [ + { + "color": "#3c3c3c" + } + ] + }, + { + "featureType": "road.highway.controlled_access", + "elementType": "geometry", + "stylers": [ + { + "color": "#4e4e4e" + } + ] + }, + { + "featureType": "road.local", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#616161" + } + ] + }, + { + "featureType": "transit", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#757575" + } + ] + }, + { + "featureType": "water", + "elementType": "geometry", + "stylers": [ + { + "color": "#000000" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#3d3d3d" + } + ] + } + ] \ No newline at end of file diff --git a/assets/json/mapstyle_light.json b/assets/json/mapstyle_light.json new file mode 100644 index 0000000..45a830e --- /dev/null +++ b/assets/json/mapstyle_light.json @@ -0,0 +1,176 @@ +[ + { + "featureType": "all", + "elementType": "all", + "stylers": [ + { + "visibility": "simplified" + }, + { + "saturation": "0" + } + ] + }, + { + "featureType": "administrative", + "elementType": "all", + "stylers": [ + { + "visibility": "on" + } + ] + }, + { + "featureType": "administrative.country", + "elementType": "all", + "stylers": [ + { + "visibility": "on" + } + ] + }, + { + "featureType": "administrative.province", + "elementType": "all", + "stylers": [ + { + "visibility": "on" + } + ] + }, + { + "featureType": "administrative.locality", + "elementType": "all", + "stylers": [ + { + "visibility": "on" + } + ] + }, + { + "featureType": "administrative.neighborhood", + "elementType": "all", + "stylers": [ + { + "visibility": "on" + } + ] + }, + { + "featureType": "administrative.land_parcel", + "elementType": "geometry", + "stylers": [ + { + "visibility": "on" + } + ] + }, + { + "featureType": "landscape", + "elementType": "all", + "stylers": [ + { + "visibility": "on" + }, + { + "color": "#F6F6F4" + } + ] + }, + { + "featureType": "landscape.man_made", + "elementType": "all", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "poi", + "elementType": "all", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "poi", + "elementType": "labels.text.stroke", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "poi.attraction", + "elementType": "all", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "poi.business", + "elementType": "all", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "road", + "elementType": "all", + "stylers": [ + { + "visibility": "on" + }, + { + "saturation": "-100" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [ + { + "color": "#ffffff" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#dfdfdf" + } + ] + }, + { + "featureType": "water", + "elementType": "all", + "stylers": [ + { + "color": "#afdef9" + } + ] + }, + { + "featureType": "water", + "elementType": "labels", + "stylers": [ + { + "visibility": "simplified" + }, + { + "color": "#ffffff" + } + ] + } +] \ No newline at end of file diff --git a/assets/marker.png b/assets/marker.png index 47606c0..35a73dc 100644 Binary files a/assets/marker.png and b/assets/marker.png differ diff --git a/assets/marker_darkmode.png b/assets/marker_darkmode.png new file mode 100644 index 0000000..ff97d3e Binary files /dev/null and b/assets/marker_darkmode.png differ diff --git a/assets/marker_selected.png b/assets/marker_selected.png index 72d250e..b78804f 100644 Binary files a/assets/marker_selected.png and b/assets/marker_selected.png differ diff --git a/assets/selectedMarker_darkmode.png b/assets/selectedMarker_darkmode.png new file mode 100644 index 0000000..3192438 Binary files /dev/null and b/assets/selectedMarker_darkmode.png differ diff --git a/example/lib/main.dart b/example/lib/main.dart index 30273ed..88916a8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -44,15 +44,6 @@ class MyHomePage extends StatelessWidget { ); }, ), - ElevatedButton( - child: Text('Stateful Widget Example'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => StatefulExample()), - ); - }, - ), ElevatedButton( child: Text('Advanced Usage'), onPressed: () { diff --git a/example/lib/stateful_example.dart b/example/lib/stateful_example.dart index ad138a3..e69de29 100644 --- a/example/lib/stateful_example.dart +++ b/example/lib/stateful_example.dart @@ -1,60 +0,0 @@ -import 'package:example/simple_usage.dart'; -import 'package:flutter/material.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:interactive_maps_marker/interactive_maps_marker.dart'; - -class StatefulExample extends StatefulWidget { - @override - _StatefulExampleState createState() => _StatefulExampleState(); -} - -class _StatefulExampleState extends State { - List markers = []; - InteractiveMapsController controller = InteractiveMapsController(); - - @override - void initState() { - super.initState(); -// Fake delay for simulating a network request - Future.delayed(Duration(seconds: 2)).then((value) { - setState(() { - markers.add(MarkerItem(id: 1, latitude: 31.4673274, longitude: 74.2637687)); - markers.add(MarkerItem(id: 2, latitude: 31.4718461, longitude: 74.3531591)); - controller.reset(index: 0); - }); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Stateful Usage'), - actions: [ - IconButton( - icon: Icon(Icons.add), - onPressed: () { - setState(() { - markers.add(MarkerItem(id: 3, latitude: 31.5325107, longitude: 74.3610325)); - markers.add(MarkerItem(id: 4, latitude: 31.4668809, longitude: 74.31354)); - controller.reset(index: 0); - }); - }, - ) - ], - ), - body: InteractiveMapsMarker( - items: markers, - controller: controller, - center: LatLng(31.4906504, 74.319872), - itemContent: (context, index) { - MarkerItem item = markers[index]; - return BottomTile(item: item); - }, - onLastItem: () { - print('Last Item'); - }, - ), - ); - } -} diff --git a/lib/helpers/map_helper.dart b/lib/helpers/map_helper.dart new file mode 100644 index 0000000..5ecb981 --- /dev/null +++ b/lib/helpers/map_helper.dart @@ -0,0 +1,170 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:fluster/fluster.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'map_marker.dart'; + +/// In here we are encapsulating all the logic required to get marker icons from url images +/// and to show clusters using the [Fluster] package. +class MapHelper { + /// If there is a cached file and it's not old returns the cached marker image file + /// else it will download the image and save it on the temp dir and return that file. + /// + /// This mechanism is possible using the [DefaultCacheManager] package and is useful + /// to improve load times on the next map loads, the first time will always take more + /// time to download the file and set the marker image. + /// + /// You can resize the marker image by providing a [targetWidth]. + static Future getMarkerImageFromAsset( + String assetPath, { + int? targetWidth, + }) async { + // Load the image asset from the project folder + final ByteData data = await rootBundle.load(assetPath); + Uint8List markerImageBytes = data.buffer.asUint8List(); + + if (targetWidth != null) { + markerImageBytes = await _resizeImageBytes( + markerImageBytes, + targetWidth, + ); + } + + return BitmapDescriptor.fromBytes(markerImageBytes); + } + + /// Draw a [clusterColor] circle with the [clusterSize] text inside that is [width] wide. + /// + /// Then it will convert the canvas to an image and generate the [BitmapDescriptor] + /// to be used on the cluster marker icons. + static Future _getClusterMarker( + int clusterSize, + Color clusterColor, + Color textColor, + int width, + ) async { + final PictureRecorder pictureRecorder = PictureRecorder(); + final Canvas canvas = Canvas(pictureRecorder); + final Paint paint = Paint()..color = clusterColor; + final TextPainter textPainter = TextPainter( + textDirection: TextDirection.ltr, + ); + + final double radius = width / 2; + + canvas.drawCircle( + Offset(radius, radius), + radius, + paint, + ); + + textPainter.text = TextSpan( + text: clusterSize.toString(), + style: TextStyle( + fontSize: radius - 5, + fontWeight: FontWeight.bold, + color: textColor, + ), + ); + + textPainter.layout(); + textPainter.paint( + canvas, + Offset(radius - textPainter.width / 2, radius - textPainter.height / 2), + ); + + final image = await pictureRecorder.endRecording().toImage( + radius.toInt() * 2, + radius.toInt() * 2, + ); + final data = await image.toByteData(format: ImageByteFormat.png); + + return BitmapDescriptor.fromBytes(data!.buffer.asUint8List()); + } + + /// Resizes the given [imageBytes] with the [targetWidth]. + /// + /// We don't want the marker image to be too big so we might need to resize the image. + static Future _resizeImageBytes( + Uint8List imageBytes, + int targetWidth, + ) async { + final Codec imageCodec = await instantiateImageCodec( + imageBytes, + targetWidth: targetWidth, + ); + + final FrameInfo frameInfo = await imageCodec.getNextFrame(); + + final data = await frameInfo.image.toByteData(format: ImageByteFormat.png); + + return data!.buffer.asUint8List(); + } + + /// Inits the cluster manager with all the [MapMarker] to be displayed on the map. + /// Here we're also setting up the cluster marker itself, also with an [clusterImageUrl]. + /// + /// For more info about customizing your clustering logic check the [Fluster] constructor. + static Future> initClusterManager( + List markers, + int minZoom, + int maxZoom, + ) async { + return Fluster( + minZoom: minZoom, + maxZoom: maxZoom, + radius: 150, + extent: 2048, + nodeSize: 64, + points: markers, + createCluster: ( + BaseCluster? cluster, + double? lng, + double? lat, + ) => + MapMarker( + id: cluster!.id.toString(), + position: LatLng(lat!, lng!), + isCluster: cluster.isCluster, + clusterId: cluster.id, + pointsSize: cluster.pointsSize, + childMarkerId: cluster.childMarkerId, + ), + ); + } + + /// Gets a list of markers and clusters that reside within the visible bounding box for + /// the given [currentZoom]. For more info check [Fluster.clusters]. + static Future> getClusterMarkers( + Fluster? clusterManager, + double currentZoom, + Color clusterColor, + Color clusterTextColor, + int clusterWidth, + ) { + if (clusterManager == null) return Future.value([]); + + return Future.wait(clusterManager.clusters( + [-180, -85, 180, 85], + currentZoom.toInt(), + ).map((mapMarker) async { + if (mapMarker.isCluster!) { + mapMarker.icon = await _getClusterMarker( + mapMarker.pointsSize!, + clusterColor, + clusterTextColor, + clusterWidth, + ); + } + + return mapMarker.toMarker(); + }).toList()); + } +} diff --git a/lib/helpers/map_marker.dart b/lib/helpers/map_marker.dart new file mode 100644 index 0000000..6e5db4a --- /dev/null +++ b/lib/helpers/map_marker.dart @@ -0,0 +1,42 @@ +import 'package:fluster/fluster.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +class MapMarker extends Clusterable { + final String id; + final LatLng position; + BitmapDescriptor? icon; + final Function()? onTap; // Add onTap callback + + MapMarker({ + required this.id, + required this.position, + this.icon, + this.onTap, // Initialize onTap property + isCluster = false, + clusterId, + pointsSize, + childMarkerId, + }) : super( + markerId: id, + latitude: position.latitude, + longitude: position.longitude, + isCluster: isCluster, + clusterId: clusterId, + pointsSize: pointsSize, + childMarkerId: childMarkerId, + ); + + Marker toMarker() { + final marker = Marker( + markerId: MarkerId(isCluster! ? 'cl_$id' : id), + position: LatLng( + position.latitude, + position.longitude, + ), + icon: icon!, + onTap: onTap, // Set the onTap callback for the Marker + ); + + return marker; + } +} diff --git a/lib/interactive_maps_controller.dart b/lib/interactive_maps_controller.dart index b6f2e31..dc0bbf5 100644 --- a/lib/interactive_maps_controller.dart +++ b/lib/interactive_maps_controller.dart @@ -11,25 +11,13 @@ class InteractiveMapsController { _state = state; } - void setCurrentIndex(int index) { - if (_state != null) { - _state?.setIndex(index); - } - } - - void rebuild([int? index]) { - _state?.setState(() { - _state?.setIndex(index ?? _state?.currentIndex ?? 0); - }); - } - void reset({int? index}) { Future.delayed(Duration(milliseconds: 200)).then((value) { _state?.pageController.jumpToPage(index ?? _state?.currentIndex ?? 0); - _state?.rebuildMarkers(index ?? _state?.currentIndex ?? 0); getMapController()?.animateCamera( CameraUpdate.newCameraPosition( - CameraPosition(target: _state!.widget.center, zoom: _state!.widget.zoom), + CameraPosition( + target: _state!.widget.center, zoom: _state!.widget.zoom), ), ); }); diff --git a/lib/interactive_maps_marker.dart b/lib/interactive_maps_marker.dart index 08ec1e2..0ceea45 100644 --- a/lib/interactive_maps_marker.dart +++ b/lib/interactive_maps_marker.dart @@ -2,21 +2,22 @@ library interactive_maps_marker; // interactive_marker_list import 'dart:async'; +import 'package:fluster/fluster.dart'; import "package:flutter/material.dart"; -import 'package:flutter/widgets.dart'; import 'package:flutter/services.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:interactive_maps_marker/interactive_maps_controller.dart'; export 'package:interactive_maps_marker/interactive_maps_controller.dart'; -import './utils.dart'; +import 'helpers/map_helper.dart'; +import 'helpers/map_marker.dart'; +import 'package:maps_toolkit/maps_toolkit.dart' as mp; class MarkerItem { int id; - double latitude; - double longitude; - - MarkerItem({required this.id, required this.latitude, required this.longitude}); + LatLng location; + String ville; + MarkerItem({required this.id, required this.location, required this.ville}); } class InteractiveMapsMarker extends StatefulWidget { @@ -31,44 +32,57 @@ class InteractiveMapsMarker extends StatefulWidget { final IndexedWidgetBuilder? itemContent; final IndexedWidgetBuilder? itemBuilder; + final IndexedWidgetBuilder? restItemBuilder; final EdgeInsetsGeometry itemPadding; final Alignment contentAlignment; + final LatLng? initialPositionFromlist; + final String? filteredCity; + final String? originalCity; + final Function(dynamic) onValueReceived; InteractiveMapsController? controller; VoidCallback? onLastItem; - - InteractiveMapsMarker({ - required this.items, - this.itemBuilder, - this.center = const LatLng(0.0, 0.0), - this.itemContent, - this.itemHeight = 116, - this.zoom = 12.0, - this.zoomFocus = 15.0, - this.zoomKeepOnTap = false, - this.itemPadding = const EdgeInsets.only(bottom: 80.0), - this.contentAlignment = Alignment.bottomCenter, - this.controller, - this.onLastItem, - }){ - if(itemBuilder == null && itemContent == null){ + final List keys; + final List remainingKeys; + InteractiveMapsMarker( + {required this.items, + Key? key, + this.itemBuilder, + required this.onValueReceived, + this.restItemBuilder, + this.center = const LatLng(0.0, 0.0), + this.itemContent, + this.itemHeight = 116, + this.zoom = 12.0, + this.zoomFocus = 15.0, + this.zoomKeepOnTap = false, + this.itemPadding = const EdgeInsets.only(bottom: 80.0), + this.contentAlignment = Alignment.bottomCenter, + this.controller, + this.onLastItem, + required this.keys, + required this.remainingKeys, + this.initialPositionFromlist, + this.filteredCity, + this.originalCity}) + : super(key: key) { + if (itemBuilder == null && itemContent == null) { throw Exception('itemBuilder or itemContent must be provided'); } - readIcons(); } - - void readIcons() async { - if (markerIcon == null) markerIcon = await getBytesFromAsset('packages/interactive_maps_marker/assets/marker.png', 100); - if (markerIconSelected == null) markerIconSelected = await getBytesFromAsset('packages/interactive_maps_marker/assets/marker_selected.png', 100); + void sendValueToParent(dynamic data) { + onValueReceived(data); } Uint8List? markerIcon; Uint8List? markerIconSelected; + Uint8List? markerIconDark; + Uint8List? markerIconSelectedDark; @override InteractiveMapsMarkerState createState() { var state = InteractiveMapsMarkerState(); - if(controller != null){ + if (controller != null) { controller!.currentState(state); } return state; @@ -79,31 +93,188 @@ class InteractiveMapsMarkerState extends State { Completer _controller = Completer(); GoogleMapController? mapController; PageController pageController = PageController(viewportFraction: 0.9); + LatLng? _initialPosition; - Set markers = {}; + List googleMarkers = []; +/* Set markers = {}; + */ int currentIndex = 0; ValueNotifier selectedMarker = ValueNotifier(0); + /// Set of displayed markers and cluster markers on the map + final Set _markers = Set(); + + /// Minimum zoom at which the markers will cluster + final int _minClusterZoom = 0; + + /// Maximum zoom at which the markers will cluster + final int _maxClusterZoom = 10; + + /// [Fluster] instance used to manage the clusters + Fluster? _clusterManager; + + /// Current map zoom. Initial zoom will be 15, street level + double _currentZoom = 15; + + /// Map loading flag + bool _isMapLoading = true; + + /// Markers loading flag + bool _areMarkersLoading = true; + bool setFromSameCity = false; + bool markerTapped = false; + bool showDetailsNabeul = false; + bool showDetailsTunis = false; + bool isNabeul = false; + bool isTunis = false; + String city = ""; + String previousCity = ""; + + /// Url image used on normal markers + /// Url image used on normal markers + final String _markerImageUrl = + 'packages/interactive_maps_marker/assets/marker.png'; + + final String _markerImageDarkUrl = + 'packages/interactive_maps_marker/assets/marker_darkmode.png'; + + /// Color of the cluster circle + final Color _clusterColor = Color(0xFFff5f5f); + + /// Color of the cluster text + final Color _clusterTextColor = Colors.white; + final List markers = []; + Map indexMapping = {}; + Map indexMappingRemaining = {}; + List polygonPoints = [ + mp.LatLng(36.53, 10.33), + mp.LatLng(36.92, 10.96), + mp.LatLng(36.66, 11.32), + mp.LatLng(36.34, 10.56), + ]; + List polygonPointsTunis = [ + mp.LatLng(36.91, 9.95), + mp.LatLng(37.00, 10.25), + mp.LatLng(36.84, 10.47), + mp.LatLng(36.62, 10.18), + ]; + + late List newMarkerPostions = []; + late int? originalIndex = null; @override void initState() { - rebuildMarkers(currentIndex); + newMarkerPostions = widget.items.map((e) => e.location).toList(); + indexMapping = Map.fromIterable(widget.keys, + key: (item) => widget.keys.indexOf(item), value: (item) => item); + indexMappingRemaining = Map.fromIterable(widget.remainingKeys, + key: (item) => widget.remainingKeys.indexOf(item), + value: (item) => item); super.initState(); } @override void didChangeDependencies() { - rebuildMarkers(currentIndex); super.didChangeDependencies(); } - void _onMapCreated(GoogleMapController controller) { - mapController = controller; - _controller.complete(controller); + int? getKeyForValue(Map map, int targetValue) { + for (var entry in map.entries) { + if (entry.value == targetValue) { + return entry.key; + } + } + return null; // Return null if the value is not found. + } + + /// Inits [Fluster] and all the markers with network images and updates the loading state. + void _initMarkers() async { + for (LatLng markerLocation in newMarkerPostions) { + final BitmapDescriptor markerImage = + await MapHelper.getMarkerImageFromAsset( + Theme.of(context).brightness == Brightness.dark + ? _markerImageDarkUrl + : _markerImageUrl, + targetWidth: 80); + markers.add( + MapMarker( + onTap: () { + int tappedIndex = newMarkerPostions.indexOf(markerLocation); + originalIndex = getKeyForValue(indexMapping, tappedIndex); + setFromSameCity = true; + setState(() { + markerTapped = true; + }); + if (getKeyForValue(indexMapping, tappedIndex) == null) { + originalIndex = + getKeyForValue(indexMappingRemaining, tappedIndex); + setFromSameCity = false; + } + if (_currentZoom > 10) { + pageController.jumpToPage( + originalIndex!, + /* duration: Duration(milliseconds: 0), + curve: Curves.bounceInOut, */ + ); + } + _pageChanged(tappedIndex!); + }, + id: newMarkerPostions.indexOf(markerLocation).toString(), + position: markerLocation, + icon: markerImage, + ), + ); + } + + _clusterManager = (await MapHelper.initClusterManager( + markers, + _minClusterZoom, + _maxClusterZoom, + )) as Fluster?; + + await _updateMarkers(); + } + + Future _updateMarkers([double? updatedZoom]) async { + if (_clusterManager == null || updatedZoom == _currentZoom) return; + if (_currentZoom <= 10) { + markerTapped = false; + } else + markerTapped = true; + + if (updatedZoom != null) { + _currentZoom = updatedZoom; + } + + setState(() { + _areMarkersLoading = true; + }); + + final updatedMarkers = await MapHelper.getClusterMarkers( + _clusterManager, + _currentZoom, + _clusterColor, + _clusterTextColor, + 80, + ); + _markers + ..clear() + ..addAll(updatedMarkers); + + setState(() { + _areMarkersLoading = false; + }); + } + + bool isInPolygon(LatLng point, List polygonPoints) { + final pointMp = mp.LatLng(point.latitude, point.longitude); + + return mp.PolygonUtil.containsLocation(pointMp, polygonPoints, false); } @override Widget build(BuildContext context) { - return StreamBuilder( + return StreamBuilder( + stream: null, initialData: 0, builder: (context, snapshot) { return Stack( @@ -115,12 +286,25 @@ class InteractiveMapsMarkerState extends State { padding: widget.itemPadding, child: SizedBox( height: widget.itemHeight, - child: PageView.builder( - itemCount: widget.items.length, - controller: pageController, - onPageChanged: _pageChanged, - itemBuilder: widget.itemBuilder != null ? widget.itemBuilder! : _buildItem, - ), + child: markerTapped && (showDetailsNabeul || showDetailsTunis) + ? PageView.builder( + itemCount: originalIndex != null && !setFromSameCity + ? indexMappingRemaining.length + : indexMapping.length, + controller: pageController, + onPageChanged: (int pageIndex) { + final NeworiginalIndex = + originalIndex != null && !setFromSameCity + ? indexMappingRemaining[pageIndex] + : indexMapping[pageIndex]; + if (NeworiginalIndex != null) { + _pageChanged(NeworiginalIndex); + } + }, + itemBuilder: originalIndex != null && !setFromSameCity + ? widget.restItemBuilder! + : widget.itemBuilder!) + : SizedBox.shrink(), ), ), ) @@ -135,55 +319,129 @@ class InteractiveMapsMarkerState extends State { child: ValueListenableBuilder( valueListenable: selectedMarker, builder: (context, value, child) { - print('Values changed'); return GoogleMap( zoomControlsEnabled: false, - markers: markers, + markers: _markers, myLocationEnabled: true, - myLocationButtonEnabled: false, - onMapCreated: _onMapCreated, + myLocationButtonEnabled: true, + padding: EdgeInsets.only( + top: 40.0, + ), + onMapCreated: (GoogleMapController controller) async { + mapController = controller; + await mapController?.setMapStyle( + await DefaultAssetBundle.of(context).loadString( + Theme.of(context).brightness != Brightness.dark + ? "assets/json/mapstyle_light.json" + : "assets/json/mapstyle_dark.json", + ), + ); + _initMarkers(); + }, initialCameraPosition: CameraPosition( - target: widget.center, + target: widget.initialPositionFromlist != null + ? widget.initialPositionFromlist as LatLng + : widget.initialPositionFromlist as LatLng, zoom: widget.zoom, ), + onCameraMove: (position) => { + setState(() { + isNabeul = isInPolygon(position.target, polygonPoints); + isTunis = isInPolygon(position.target, polygonPointsTunis); + city = isNabeul + ? "Nabeul" + : (isTunis ? "Tunis" : "different city"); + + showDetailsNabeul = isNabeul; + showDetailsTunis = isTunis; + if (widget.originalCity == "Nabeul Governorate") { + if (widget.filteredCity == "Tunis Governorate" && + isTunis /* widget.originalCity != "Tunis Governorate" */) { + print("here"); + setFromSameCity = false; + originalIndex = 1; + } + } else if (widget.originalCity == "Tunis Governorate") { + if (widget.filteredCity == "Nabeul Governorate" && + isNabeul /* widget.originalCity != "Tunis Governorate" */) { + print("here"); + setFromSameCity = false; + originalIndex = 1; + } + } + /* if (widget.filteredCity == "Nabeul Governorate" && + isTunis && + widget.originalCity != "Nabeul Governorate") { + print("here"); + setFromSameCity = false; + originalIndex = 1; + } */ + if (widget.originalCity == "Tunis Governorate") { + if (city != previousCity) { + if (previousCity == "different city") { + if (city == "Nabeul") { + // City changed from Tunis to Nabeul + setFromSameCity = false; + originalIndex = 1; + print("City changed from Tunis to Nabeul"); + } else if (city == "Tunis") { + // City changed from Nabeul to Tunis + setFromSameCity = true; + originalIndex = 1; + print("City changed from Nabeul to Tunis"); + } + } + + // Update the previousCity + previousCity = city; + } + } + if (widget.originalCity == "Nabeul Governorate") { + if (city != previousCity) { + if (previousCity == "different city") { + if (city == "Nabeul") { + // City changed from Tunis to Nabeul + setFromSameCity = true; + originalIndex = 1; + print("City changed from Tunis to Nabeul"); + } else if (city == "Tunis") { + // City changed from Nabeul to Tunis + setFromSameCity = false; + originalIndex = 1; + print("City changed from Nabeul to Tunis"); + } + } + + // Update the previousCity + previousCity = city; + } + } + }), + widget.sendValueToParent(city), + _updateMarkers(position.zoom), + }, ); }, ), ); } - Widget? _buildItem(BuildContext context, int i) { - return Transform.scale( - scale: i == currentIndex ? 1 : 0.9, - child: ClipRRect( - borderRadius: BorderRadius.circular(10.0), - child: Container( - height: widget.itemHeight, - decoration: BoxDecoration( - color: Color(0xffffffff), - boxShadow: [ - BoxShadow( - offset: Offset(0.5, 0.5), - color: Color(0xff000000).withOpacity(0.12), - blurRadius: 20, - ), - ], - ), - child: widget.itemContent!(context, i), - ), - ), - ); - } - void _pageChanged(int index) { try { setState(() => currentIndex = index); - if(widget.onLastItem != null && index == widget.items.length - 1){ + if (widget.onLastItem != null && index == widget.items.length - 1) { widget.onLastItem!(); } - rebuildMarkers(index); - Marker marker = markers.elementAt(index); - + Marker marker = markers.elementAt(index).toMarker(); + if (_currentZoom <= 10) { + Future.delayed(Duration(milliseconds: 500), () { + pageController.animateToPage( + index, + duration: Duration(milliseconds: 500), + curve: Curves.bounceInOut, + ); + }); + } mapController ?.animateCamera( widget.zoomKeepOnTap @@ -201,47 +459,4 @@ class InteractiveMapsMarkerState extends State { print(e); } } - - Future rebuildMarkers(int index) async { - if(widget.items.length == 0) return; - int current = widget.items[index].id; - - Set _markers = Set(); - - widget.items.forEach((item) { - _markers.add( - Marker( - markerId: MarkerId(item.id.toString()), - position: LatLng(item.latitude, item.longitude), - onTap: () { - int tappedIndex = widget.items.indexWhere((element) => element.id == item.id); - pageController.animateToPage( - tappedIndex, - duration: Duration(milliseconds: 300), - curve: Curves.bounceInOut, - ); - _pageChanged(tappedIndex); - }, - icon: BitmapDescriptor.defaultMarkerWithHue(item.id == current ? BitmapDescriptor.hueGreen : BitmapDescriptor.hueRed), - // icon: item.id == current ? BitmapDescriptor.fromBytes(widget.markerIconSelected!) : BitmapDescriptor.fromBytes(widget.markerIcon!), - ), - ); - }); - - setState(() { - markers = _markers; - }); - // selectedMarker.value = current; - selectedMarker.value = current; - // selectedMarker.notifyListeners(); - } - - void setIndex(int index){ - pageController.animateToPage( - index, - duration: Duration(milliseconds: 300), - curve: Curves.bounceInOut, - ); - _pageChanged(index); - } } diff --git a/pubspec.lock b/pubspec.lock index 911f33b..bef6e96 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: @@ -37,10 +37,26 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" + source: hosted + version: "1.17.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.0.0" fake_async: dependency: transitive description: @@ -49,72 +65,165 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fluster: + dependency: "direct main" + description: + name: fluster + sha256: "3807f5d088b7798f0416b8578498046338af98bb4fb922a70e2810b8293963f6" + url: "https://pub.dev" + source: hosted + version: "1.2.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: c224ac897bed083dabf11f238dd11a239809b446740be0c2044608c50029ffdf + sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.16" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + google_maps: + dependency: transitive + description: + name: google_maps + sha256: "555d5d736339b0478e821167ac521c810d7b51c3b2734e6802a9f046b64ea37a" + url: "https://pub.dev" + source: hosted + version: "6.3.0" google_maps_flutter: dependency: "direct main" description: name: google_maps_flutter - sha256: "24392ef192f3b00bcd93151375676805a9933574423a5bd5509a0ead2e8a4215" + sha256: d4914cb38b3dcb62c39c085d968d434de0f8050f00f4d9f5ba4a7c7e004934cb url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.5.0" google_maps_flutter_android: dependency: transitive description: name: google_maps_flutter_android - sha256: ee3c1a63983b8ba17a9a7c1233c3542fbfbc0ebf540d3aa9b8fd5162681e0219 + sha256: e6cb018169e49332f88d23b1d2119b09e8ab4e7d3a1b889a1b7b3fd113e034ba url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.5.1" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios - sha256: e9ad74415a222573625a2c1717adc1e375b18e8ce660fc12db734d1bda1132d4 + sha256: "2aa28eb9b9d5dfdce6932a7b7f096430bf83a1a09b4e21e81939351f407c787f" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.2" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface - sha256: a07811d2b82055815ede75e1fe4b7b76f71a0b4820b26f71bdaddd157d6a3e20 + sha256: a3e9e6896501e566d902c6c69f010834d410ef4b7b5c18b90c77e871c86b7907 + url: "https://pub.dev" + source: hosted + version: "2.4.1" + google_maps_flutter_web: + dependency: transitive + description: + name: google_maps_flutter_web + sha256: "05067c5aa762ebee44b7ef4902a311ed8cf891ef655e2798bae063aa3050c8d9" + url: "https://pub.dev" + source: hosted + version: "0.5.4+1" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "2.2.6" + version: "4.0.2" js: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + js_wrapping: + dependency: transitive + description: + name: js_wrapping + sha256: e385980f7c76a8c1c9a560dfb623b890975841542471eade630b2871d243851c + url: "https://pub.dev" + source: hosted + version: "0.7.4" + maps_toolkit: + dependency: "direct main" + description: + name: maps_toolkit + sha256: "277877f9505208acacd2a0794ef190e836a5ffee58ebc8efc5b9ca8de50e3e2f" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "3.0.0" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -127,26 +236,98 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" path: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + url: "https://pub.dev" + source: hosted + version: "3.1.3" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.6" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + sanitize_html: + dependency: transitive + description: + name: sanitize_html + sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989" + url: "https://pub.dev" + source: hosted + version: "2.1.0" sky_engine: dependency: transitive description: flutter @@ -160,6 +341,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" stack_trace: dependency: transitive description: @@ -192,6 +397,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" + source: hosted + version: "3.1.0" term_glyph: dependency: transitive description: @@ -204,10 +417,26 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7 + url: "https://pub.dev" + source: hosted + version: "4.1.0" vector_math: dependency: transitive description: @@ -216,6 +445,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.dev" + source: hosted + version: "5.0.9" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + url: "https://pub.dev" + source: hosted + version: "1.0.3" sdks: - dart: ">=2.19.0 <3.0.0" - flutter: ">=3.3.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index fd97c81..fcd5357 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,9 @@ dependencies: flutter: sdk: flutter google_maps_flutter: ^2.2.5 + flutter_cache_manager: ^3.0.2 + fluster: any + maps_toolkit: ^3.0.0 dev_dependencies: flutter_test: @@ -18,4 +21,6 @@ dev_dependencies: flutter: assets: - - assets/ \ No newline at end of file + - assets/ + - assets/marker.png + - assets/marker_darkmode.png \ No newline at end of file