Skip to content

Conversation

@kartikeyg0104
Copy link

@kartikeyg0104 kartikeyg0104 commented Dec 13, 2025

closes #24

Overview

This PR implements a comprehensive dynamic map feature that allows users to explore trees around them using an interactive map interface. The implementation uses geohash-based spatial indexing for efficient tree queries and flutter_map (open-source) for map rendering.

🎯 Problem Statement

"By fetching the NFTs through the blockchain by referencing through geohash or any other smart and more importantly efficient way... A user should be able to see trees around him on a dynamic map."

✨ Features Implemented

1. Interactive Map Page (/explore-map)

  • Full-screen interactive map using OpenStreetMap tiles
  • User location tracking with permission handling
  • Tree markers with clustering at lower zoom levels
  • Tap-to-view tree details panel

2. Geohash-Based Spatial Indexing

  • Efficient O(1) tree lookups by geographic area
  • Automatic precision adjustment based on zoom level
  • Neighbor geohash calculation for area queries

3. Tree Clustering

  • Groups nearby trees at lower zoom levels
  • Shows tree count in cluster bubbles
  • Expands to individual markers on zoom

4. Filtering & Search

  • Filter by: Alive/Deceased, Species, Care count, Planting date
  • Search by: Tree ID, Species name, Geohash, Coordinates

5. Nearby Trees Widget

  • Shows trees near user's current location
  • Displays distance to each tree
  • Horizontal scrollable card list

🏗️ Architecture

System Flow Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│                              USER INTERFACE                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐        │
│  │  ExploreTreesMap │    │  NearbyTrees    │    │  TreesPage      │        │
│  │  Page            │    │  Widget         │    │  (Updated)      │        │
│  └────────┬────────┘    └────────┬────────┘    └────────┬────────┘        │
│           │                      │                      │                  │
│           └──────────────────────┼──────────────────────┘                  │
│                                  │                                          │
└──────────────────────────────────┼──────────────────────────────────────────┘
                                   │
                                   ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                              SERVICES LAYER                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                        TreeMapService                                │   │
│  │  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐     │   │
│  │  │ fetchAllTrees() │  │ clusterTrees()  │  │ getTreesNear    │     │   │
│  │  │                 │  │                 │  │ Location()      │     │   │
│  │  └────────┬────────┘  └────────┬────────┘  └────────┬────────┘     │   │
│  │           │                    │                    │               │   │
│  │           └────────────────────┼────────────────────┘               │   │
│  │                                │                                     │   │
│  │                    ┌───────────▼───────────┐                        │   │
│  │                    │    Tree Cache         │                        │   │
│  │                    │  Map<geohash, trees>  │                        │   │
│  │                    │  Set<geohash, ids>    │ ◄── O(1) lookups      │   │
│  │                    └───────────────────────┘                        │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                        GeohashService                                │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌────────────┐ │   │
│  │  │ encode()    │  │ decode()    │  │ getNeighbors│  │ getBounds()│ │   │
│  │  │ lat,lng→hash│  │ hash→lat,lng│  │ (native)    │  │            │ │   │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  └────────────┘ │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                              DATA LAYER                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                    Blockchain (Smart Contract)                       │   │
│  │                                                                      │   │
│  │  getRecentTreesPaginated(offset, limit)                             │   │
│  │       │                                                              │   │
│  │       ▼                                                              │   │
│  │  ┌─────────────────────────────────────────────────────────────┐    │   │
│  │  │  Tree NFT Data                                               │    │   │
│  │  │  - id, latitude, longitude, species, imageUri               │    │   │
│  │  │  - geoHash, careCount, plantingDate, numberOfTrees          │    │   │
│  │  └─────────────────────────────────────────────────────────────┘    │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Geohash Spatial Indexing

┌─────────────────────────────────────────────────────────────────────────────┐
│                         GEOHASH PRECISION LEVELS                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Precision 1: ~5000km x 5000km     Precision 4: ~39km x 19km               │
│  ┌───────────────────────┐         ┌───────────────────────┐               │
│  │                       │         │ ┌───┬───┬───┬───┐     │               │
│  │          9            │         │ │   │   │   │   │     │               │
│  │                       │         │ ├───┼───┼───┼───┤     │               │
│  │                       │         │ │   │ X │   │   │     │               │
│  └───────────────────────┘         │ ├───┼───┼───┼───┤     │               │
│                                    │ │   │   │   │   │     │               │
│  Precision 6: ~1.2km x 0.6km       │ └───┴───┴───┴───┘     │               │
│  ┌───────────────────────┐         └───────────────────────┘               │
│  │┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐ │                                                  │
│  ││ │ │ │ │ │ │ │ │ │ │ │         Zoom Level → Precision Mapping:          │
│  │├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤ │         ┌────────────────────────────┐          │
│  ││ │ │ │🌳│ │ │ │ │ │ │ │         │ Zoom ≥18 → Precision 8     │          │
│  │├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤ │         │ Zoom ≥16 → Precision 7     │          │
│  ││ │ │ │ │ │ │ │ │ │ │ │         │ Zoom ≥14 → Precision 6     │          │
│  │├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤ │         │ Zoom ≥12 → Precision 5     │          │
│  ││ │ │ │ │ │ │ │ │ │ │ │         │ Zoom ≥10 → Precision 4     │          │
│  │└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘ │         │ Zoom ≥8  → Precision 3     │          │
│  └───────────────────────┘         └────────────────────────────┘          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Tree Clustering Algorithm

┌─────────────────────────────────────────────────────────────────────────────┐
│                         CLUSTERING BY ZOOM LEVEL                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  LOW ZOOM (< 15)                      HIGH ZOOM (≥ 15)                      │
│  ┌─────────────────────┐              ┌─────────────────────┐              │
│  │                     │              │                     │              │
│  │    ┌───┐            │              │  🌳    🌳           │              │
│  │    │ 5 │            │              │                     │              │
│  │    └───┘            │     ──►      │     🌳   🌳  🌳     │              │
│  │         ┌───┐       │   ZOOM IN    │                     │              │
│  │         │ 3 │       │              │                     │              │
│  │         └───┘       │              │                     │              │
│  └─────────────────────┘              └─────────────────────┘              │
│                                                                             │
│  Clusters show count                  Individual tree markers               │
│  Tap to zoom in                       Tap to view details                   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Data Flow for Map Interaction

┌─────────────────────────────────────────────────────────────────────────────┐
│                           USER INTERACTION FLOW                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐          │
│  │  User    │     │  Map     │     │  Tree    │     │ Blockchain│          │
│  │  Action  │     │  Widget  │     │  Service │     │  Contract │          │
│  └────┬─────┘     └────┬─────┘     └────┬─────┘     └────┬─────┘          │
│       │                │                │                │                  │
│       │ Pan/Zoom       │                │                │                  │
│       │───────────────►│                │                │                  │
│       │                │                │                │                  │
│       │                │ onPositionChanged               │                  │
│       │                │───────────────►│                │                  │
│       │                │                │                │                  │
│       │                │                │ Check cache    │                  │
│       │                │                │───────┐        │                  │
│       │                │                │       │        │                  │
│       │                │                │◄──────┘        │                  │
│       │                │                │                │                  │
│       │                │                │ Cache miss?    │                  │
│       │                │                │───────────────►│                  │
│       │                │                │                │                  │
│       │                │                │◄───────────────│                  │
│       │                │                │  Tree data     │                  │
│       │                │                │                │                  │
│       │                │◄───────────────│                │                  │
│       │                │ Clustered trees│                │                  │
│       │                │                │                │                  │
│       │◄───────────────│                │                │                  │
│       │ Updated markers│                │                │                  │
│       │                │                │                │                  │
└─────────────────────────────────────────────────────────────────────────────┘

🔧 Technical Highlights

Performance Optimizations

  • O(1) duplicate checks using Set<int> for tree IDs per geohash
  • Single-pass tree organization instead of O(n×m) nested loops
  • Lazy loading with pagination support
  • Geohash caching to avoid redundant blockchain calls

Safety Measures

  • Mounted checks after all async operations
  • Map ready guards to prevent accessing uninitialized camera
  • Sentinel pattern in copyWith for nullable filter clearing
  • Safe substring handling for short geohashes

Code Quality

  • Uses dart_geohash's native neighbor() function
  • Proper error handling with user-friendly messages
  • Clean separation of concerns (services, widgets, pages)

Summary by CodeRabbit

  • New Features
    • New Explore Map page with interactive map, clustering, single-tree markers, heatmap overlay, density legend, and incremental loading
    • Map search, nearby-trees panel, and quick filters (species, alive/deceased, care level, planting dates)
    • Quick Action buttons added to home and trees screens for map navigation
    • Wallet-gated map behavior with prompts, map controls (zoom/center/refresh), stats card, and tree detail panel/navigation

✏️ Tip: You can customize this high-level summary in your review settings.

- Add ExploreTreesMapPage with interactive flutter_map integration
- Implement GeohashService for efficient spatial queries and tree clustering
- Add TreeMapService for blockchain data fetching with geohash caching
- Create NearbyTreesWidget showing trees around user's location
- Add MapFilterWidget with species, status, and date filters
- Add MapSearchWidget for searching trees by ID, species, or coordinates
- Add TreeHeatmapLayer for density visualization
- Update home page with quick actions and nearby trees section
- Update trees page with 'Explore Map' button
- Add route for /explore-map

Features:
- Geohash-based spatial indexing for efficient tree queries
- Tree clustering at lower zoom levels for better performance
- Real-time location tracking with user position marker
- Filter trees by alive/deceased status, species, care count
- Search by tree ID, species name, geohash, or coordinates
- Responsive tree detail panel with quick navigation
- Pagination support for loading large datasets
- Cache management for optimized data fetching
- Add proper paint() implementation with radial gradient circles
- Use MapCamera for coordinate-to-screen conversion
- Add additive blending for overlapping heat points
- Filter points to only render visible area for performance
- Update shouldRepaint to check camera position changes
- Add mounted check at start of _loadNearbyTrees
- Add mounted check after getCurrentLocationWithTimeout await
- Add mounted check after getTreesNearLocation await
- Add mounted check in catch block before setState
- Prevents setState on unmounted widget errors
- Use dart_geohash native neighbor() function instead of custom delta-based calculation
- Add O(1) duplicate check using Set for tree cache (fix O(n×m) performance issue)
- Add sentinel pattern in MapFilterOptions.copyWith to allow clearing nullable fields
- Fix unsafe substring call for short geohashes in tree details panel
- Fix map controller race condition with _mapReady flag and pending center
- Remove unused _geohashService field from ExploreTreesMapPage
Prevents accessing _mapController.camera before map is initialized,
avoiding potential errors when called from _loadTrees() before _onMapReady()
@coderabbitai
Copy link

coderabbitai bot commented Dec 13, 2025

Warning

Rate limit exceeded

@kartikeyg0104 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 4 minutes and 24 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between e1833f5 and 642aba8.

📒 Files selected for processing (1)
  • lib/services/tree_map_service.dart (1 hunks)

Walkthrough

Adds a new Explore Trees map feature: route and page, map-backed tree fetching and clustering services, geohash utilities, map UI widgets (filters, search, heatmap, nearby trees), and integrations from home/trees/recent widgets to navigate to the map.

Changes

Cohort / File(s) Summary
Routing & Entry
lib/main.dart, lib/utils/constants/route_constants.dart
Adds exploreMap/exploreMapPath route constants and a new GoRoute for /explore-map that builds ExploreTreesMapPage.
Main Map Page
lib/pages/explore_trees_map_page.dart
New stateful ExploreTreesMapPage with flutter_map controller, wallet gating, incremental tree loading, clustering, filters, search, UI overlays, markers, and navigation to tree details.
Map Services
lib/services/geohash_service.dart, lib/services/tree_map_service.dart
New GeohashService (encode/decode, bounds, neighbors, precision, distance) and TreeMapService with MapTreeData/TreeCluster, geohash-based caching, blockchain fetch/pagination, clustering, and proximity queries.
Map Widgets & UX
lib/widgets/map_widgets/map_filter_widget.dart, lib/widgets/map_widgets/map_search_widget.dart, lib/widgets/map_widgets/tree_heatmap_layer.dart, lib/widgets/map_widgets/nearby_trees_widget.dart
Adds MapFilterOptions/MapFilterWidget/QuickFilterBar, MapSearchWidget/MapSearchResult, heatmap/density overlay and legend, and NearbyTreesWidget to show nearby trees with location/wallet checks.
Existing Page Integrations
lib/pages/home_page.dart, lib/pages/trees_page.dart, lib/widgets/nft_display_utils/recent_trees_widget.dart
Adds Quick Actions + Nearby Trees on home (conditional on wallet), an "Explore Trees on Map" action on trees page, and updates recent trees widget to navigate to /explore-map.
sequenceDiagram
    participant User
    participant ExploreMapPage
    participant WalletProvider
    participant TreeMapService
    participant GeohashService
    participant Blockchain

    User->>ExploreMapPage: Open /explore-map
    ExploreMapPage->>WalletProvider: Check connection
    alt wallet not connected
        ExploreMapPage->>User: Show connect-wallet prompt
    else wallet connected
        ExploreMapPage->>TreeMapService: fetchTreesInBounds(SW,NE,zoom)
        TreeMapService->>GeohashService: getGeohashesInBounds(SW,NE,precision)
        GeohashService-->>TreeMapService: geohash prefixes
        TreeMapService->>TreeMapService: check cache for prefixes
        loop for uncached prefixes
            TreeMapService->>Blockchain: fetch trees for prefix(es)
            Blockchain-->>TreeMapService: raw tree data
            TreeMapService->>TreeMapService: convert, cache by geohash
        end
        TreeMapService-->>ExploreMapPage: List<MapTreeData>
        ExploreMapPage->>TreeMapService: clusterTrees(trees, zoom)
        TreeMapService-->>ExploreMapPage: List<TreeCluster>
        ExploreMapPage->>User: Render clusters/markers, heatmap, overlays
    end

    User->>ExploreMapPage: Pan/zoom or apply filter/search
    ExploreMapPage->>TreeMapService: fetch/cluster as needed
    TreeMapService-->>ExploreMapPage: Updated trees/clusters
    ExploreMapPage->>User: Updated map display
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus review on:
    • Geohash precision, neighbor edge cases, and bounding calculations (lib/services/geohash_service.dart)
    • TreeMapService caching, pagination, geohash-prefix fetch logic, and clustering correctness (lib/services/tree_map_service.dart)
    • Map lifecycle and state management in ExploreTreesMapPage (map-ready handling, pending center/zoom, performance)
    • Integration points for widgets (filter/search/nearby) and navigation links from home_page.dart, trees_page.dart, and recent_trees_widget.dart

Possibly related PRs

  • Consistent UI  #15 — overlaps on theming and WalletProvider usage which this PR also integrates into map and widgets.
  • frontend revamp #4 — related routing and router setup changes affecting GoRoute and RouteConstants.

Suggested reviewers

  • Zahnentferner
  • ceilican

Poem

🐇 I hopped through geohashes, soft and spry,
I clustered leaves beneath the sky,
I drew green heat where saplings hide,
I nudged a map to find each stride,
Hop, search, and nibble — forest found with glee.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a dynamic tree map with geohash-based efficient fetching, which is the primary objective of this PR.
Linked Issues check ✅ Passed The PR comprehensively implements all requirements from issue #24: dynamic location-aware map, flutter_map with OpenStreetMap tiles, geohash-based spatial indexing for efficient blockchain queries, interactive features (markers, clustering, detail views), and location tracking.
Out of Scope Changes check ✅ Passed All changes are in-scope: new map components, geohash/tree services, filter widgets, and integration in home/trees pages. UI updates in home_page.dart and trees_page.dart properly support the map feature without introducing unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (14)
lib/widgets/nft_display_utils/recent_trees_widget.dart (1)

375-376: Consider using RouteConstants for maintainability.

The route is hardcoded as '/explore-map'. For consistency and maintainability, consider using RouteConstants.exploreMapPath instead, which was added in this PR.

-                      // Navigate to explore map page
-                      context.push('/explore-map');
+                      context.push(RouteConstants.exploreMapPath);
lib/pages/home_page.dart (1)

75-103: Use RouteConstants for route paths.

The route paths are hardcoded as strings. For consistency and maintainability, use the RouteConstants defined in the PR.

             child: _buildActionButton(
               context,
               icon: Icons.map,
               label: 'Explore Map',
-              onTap: () => context.push('/explore-map'),
+              onTap: () => context.push(RouteConstants.exploreMapPath),
               isPrimary: true,
             ),
           ),
           const SizedBox(width: 12),
           Expanded(
             child: _buildActionButton(
               context,
               icon: Icons.forest,
               label: 'All Trees',
-              onTap: () => context.push('/trees'),
+              onTap: () => context.push(RouteConstants.allTreesPath),
               isPrimary: false,
             ),
           ),
lib/pages/trees_page.dart (1)

84-112: Use RouteConstants for the route path.

The route path is hardcoded. For consistency with the rest of the codebase, use RouteConstants.exploreMapPath.

               onPressed: () {
-                context.push('/explore-map');
+                context.push(RouteConstants.exploreMapPath);
               },
lib/widgets/map_widgets/nearby_trees_widget.dart (2)

217-236: Use RouteConstants for navigation.

The routes are hardcoded. For consistency, use the appropriate RouteConstants.

             ElevatedButton.icon(
-              onPressed: () => context.push('/mint-nft'),
+              onPressed: () => context.push(RouteConstants.mintNftPath),
               icon: const Icon(Icons.add),
               label: const Text('Plant Tree'),
               style: ElevatedButton.styleFrom(
                 backgroundColor: getThemeColors(context)['primary'],
                 foregroundColor: Colors.white,
               ),
             ),
             const SizedBox(width: 12),
             OutlinedButton.icon(
-              onPressed: () => context.push('/explore-map'),
+              onPressed: () => context.push(RouteConstants.exploreMapPath),
               icon: const Icon(Icons.map),
               label: const Text('Explore Map'),
             ),

266-274: Use RouteConstants for the route path.

For consistency with other navigation calls, use RouteConstants.exploreMapPath.

             TextButton(
-              onPressed: () => context.push('/explore-map'),
+              onPressed: () => context.push(RouteConstants.exploreMapPath),
               child: Text(
                 'View All',
                 style: TextStyle(
                   color: getThemeColors(context)['primary'],
                 ),
               ),
             ),
lib/pages/explore_trees_map_page.dart (5)

55-71: Add mounted check before initial setState and surface initialization errors.

The first setState at line 56 executes synchronously in initState context so it's safe, but the catch block silently swallows errors without setting _errorMessage, leaving users unaware of initialization failures.

     } catch (e) {
       logger.e('Error initializing map: $e');
+      if (mounted) {
+        setState(() {
+          _errorMessage = 'Failed to initialize map: $e';
+        });
+      }
     } finally {

220-231: Shallow equality check may miss content changes.

The length-based comparison at line 228 won't detect when species names change but count remains the same. Consider using listEquals or comparing contents if this edge case matters.

-    if (_availableSpecies.length != species.length) {
-      _availableSpecies = species;
-    }
+    // Using simple assignment; list comparison overhead not worth it for this use case
+    _availableSpecies = species;

254-274: Consider wrapping pagination call in try-catch for robustness.

_onMapMove is an async method called from the map's onPositionChanged callback. While fetchAllTrees has internal error handling, exceptions from Provider.of or state updates could go unhandled.

   Future<void> _onMapMove() async {
     _updateVisibleTrees();
     
     // Load more trees if needed
     if (!_isLoadingMore && _treeMapService.allTrees.length < _treeMapService.totalTreeCount) {
-      setState(() => _isLoadingMore = true);
-      
-      final walletProvider = Provider.of<WalletProvider>(context, listen: false);
-      await _treeMapService.fetchAllTrees(
-        walletProvider: walletProvider,
-        offset: _treeMapService.allTrees.length,
-        limit: 50,
-      );
-      
-      _updateVisibleTrees();
-      
-      if (mounted) {
-        setState(() => _isLoadingMore = false);
+      try {
+        setState(() => _isLoadingMore = true);
+        
+        final walletProvider = Provider.of<WalletProvider>(context, listen: false);
+        await _treeMapService.fetchAllTrees(
+          walletProvider: walletProvider,
+          offset: _treeMapService.allTrees.length,
+          limit: 50,
+        );
+        
+        _updateVisibleTrees();
+      } finally {
+        if (mounted) {
+          setState(() => _isLoadingMore = false);
+        }
       }
     }
   }

380-385: Add const to TextStyle for minor optimization.

                   Text(
                     'Loading trees...',
-                    style: TextStyle(
+                    style: const TextStyle(
                       color: Colors.white,
                       fontSize: 16,
                     ),

670-687: Zoom controls bypass map-ready guard.

Lines 675 and 685 directly call _mapController.move() without checking _mapReady, which could throw if triggered before the map is initialized. Consider guarding or using _moveMapTo with current center.

         _buildControlButton(
           context,
           icon: Icons.add,
           onTap: () {
+            if (!_mapReady) return;
             final newZoom = (_currentZoom + 1).clamp(3.0, 18.0);
             _mapController.move(_mapController.camera.center, newZoom);
           },
         ),
         const SizedBox(height: 8),
         // Zoom out
         _buildControlButton(
           context,
           icon: Icons.remove,
           onTap: () {
+            if (!_mapReady) return;
             final newZoom = (_currentZoom - 1).clamp(3.0, 18.0);
             _mapController.move(_mapController.camera.center, newZoom);
           },
         ),
lib/widgets/map_widgets/map_filter_widget.dart (1)

83-94: Consider syncing with parent when initialOptions changes.

If the parent widget updates initialOptions (e.g., resetting filters externally), _options won't update since it's only set in initState. Add didUpdateWidget if external resets are expected.

   @override
   void initState() {
     super.initState();
     _options = widget.initialOptions;
   }

+  @override
+  void didUpdateWidget(MapFilterWidget oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.initialOptions != oldWidget.initialOptions) {
+      _options = widget.initialOptions;
+    }
+  }
+
   void _updateOptions(MapFilterOptions newOptions) {
lib/widgets/map_widgets/tree_heatmap_layer.dart (1)

109-116: Consider comparing tree list identity for more robust repaint detection.

shouldRepaint compares only trees.length, which could miss repaint when trees are replaced with a same-sized list. If this causes stale rendering, compare list identity or use a version counter.

   @override
   bool shouldRepaint(covariant _HeatmapPainter oldDelegate) {
-    return trees.length != oldDelegate.trees.length ||
+    return trees != oldDelegate.trees ||
         camera.zoom != oldDelegate.camera.zoom ||
         camera.center != oldDelegate.camera.center ||
         opacity != oldDelegate.opacity ||
         radius != oldDelegate.radius;
   }
lib/services/tree_map_service.dart (2)

246-263: Comment claims O(n) but implementation is O(n×m).

The nested loop at lines 256-262 iterates over both trees and geohashes, making it O(n×m) rather than the claimed "O(n) instead of O(n×m)". The performance is acceptable since m (geohashes in view) is typically small, but the comment is misleading.

-      // Convert geohashes to Set for O(1) prefix matching
-      final geohashSet = geohashes.toSet();
-
-      // Single pass over all trees - O(n) instead of O(n×m)
+      // Single pass over trees, checking against each requested geohash
+      // O(n×m) where m is typically small (visible geohashes)
       for (final tree in _allTrees) {

315-316: neighborGeohashes is computed but never used.

The neighbor geohashes are calculated at line 316 but not utilized for filtering or cache lookup, making this dead code. Either remove it or use it to optimize the search.

   Future<List<MapTreeData>> getTreesNearLocation({
     required WalletProvider walletProvider,
     required double latitude,
     required double longitude,
     double radiusMeters = 5000,
   }) async {
-    final centerGeohash = _geohashService.encode(latitude, longitude);
-    final neighborGeohashes = _geohashService.getNeighbors(centerGeohash);

     // Ensure we have trees loaded
     if (_allTrees.isEmpty) {
       await fetchAllTrees(walletProvider: walletProvider, limit: 100);
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02cb1fd and d04da04.

📒 Files selected for processing (12)
  • lib/main.dart (2 hunks)
  • lib/pages/explore_trees_map_page.dart (1 hunks)
  • lib/pages/home_page.dart (3 hunks)
  • lib/pages/trees_page.dart (1 hunks)
  • lib/services/geohash_service.dart (1 hunks)
  • lib/services/tree_map_service.dart (1 hunks)
  • lib/utils/constants/route_constants.dart (1 hunks)
  • lib/widgets/map_widgets/map_filter_widget.dart (1 hunks)
  • lib/widgets/map_widgets/map_search_widget.dart (1 hunks)
  • lib/widgets/map_widgets/nearby_trees_widget.dart (1 hunks)
  • lib/widgets/map_widgets/tree_heatmap_layer.dart (1 hunks)
  • lib/widgets/nft_display_utils/recent_trees_widget.dart (1 hunks)
🔇 Additional comments (41)
lib/main.dart (1)

6-6: LGTM!

The route is properly configured with appropriate constants and follows the established routing pattern in the application.

Also applies to: 72-78

lib/utils/constants/route_constants.dart (1)

4-4: LGTM!

The route constants are properly defined and follow the existing naming conventions in the file.

Also applies to: 12-12

lib/pages/home_page.dart (1)

33-57: LGTM!

The conditional rendering based on wallet connection state is appropriate, and the NearbyTreesWidget parameters (10km radius, 8 max trees) provide a good user experience.

lib/widgets/map_widgets/nearby_trees_widget.dart (2)

44-106: LGTM!

Excellent async safety with mounted checks after every await. The 15-second timeout for location fetching is appropriate, and error handling is comprehensive.


108-113: LGTM!

The distance formatting logic is clear and user-friendly, correctly handling both meter and kilometer ranges.

lib/widgets/map_widgets/map_search_widget.dart (4)

51-61: LGTM!

The focus listener correctly uses a mounted check after the delayed setState, preventing potential issues if the widget is disposed during the delay.


79-93: LGTM!

The tree ID search correctly uses regex validation and safe parsing with int.tryParse. The logic is sound.


128-140: LGTM!

The coordinate parsing includes proper validation for latitude and longitude ranges, ensuring only valid geographic coordinates are accepted.


96-109: LGTM!

The species search appropriately limits results to 5 items and includes duplicate checking to ensure a tree doesn't appear multiple times in the results.

lib/services/geohash_service.dart (4)

16-24: LGTM!

The coordinate order swapping correctly handles the GeoHasher API's longitude-first convention, returning proper LatLng objects with latitude-first ordering.


56-85: LGTM!

The neighbor calculation properly uses the dart_geohash library's Direction enum and includes appropriate error handling for edge cases at poles and the date line.


88-101: Good overlap strategy for complete coverage.

The 0.8 step multiplier ensures geohash cells overlap, providing complete coverage of the bounding box at the cost of some redundancy. This is a reasonable trade-off for spatial queries.


104-113: LGTM!

The zoom-to-precision mapping appropriately increases spatial resolution at higher zoom levels, providing efficient geohash granularity for the map visualization.

lib/pages/explore_trees_map_page.dart (17)

1-21: LGTM!

Clean imports and standard StatefulWidget definition. The page structure follows Flutter conventions.


23-46: LGTM!

Good use of the _mapReady flag with _pendingCenter/_pendingZoom to handle the race condition between map initialization and location retrieval. The sentinel pattern prevents premature map operations.


73-94: LGTM!

Proper mounted check after async operation and graceful fallback to default center on location failure.


96-123: LGTM!

The deferred move pattern with _pendingCenter/_pendingZoom correctly handles the async timing between location acquisition and map readiness.


125-152: LGTM!

Proper wallet connectivity check and error handling. The _updateVisibleTrees call at line 143 is protected by the method's internal mounted guard.


154-185: LGTM!

Proper guards for mounted and _mapReady prevent accessing the camera before initialization. The bounds filtering and clustering logic is clear.


187-218: LGTM!

Filter logic correctly handles status, species, care count, and date range filters. The timestamp conversion from seconds to milliseconds is correct.


233-252: LGTM!

Filter and search result handlers are straightforward. Different zoom levels for tree vs. non-tree results provide appropriate detail.


276-295: LGTM!

Cluster tap behavior appropriately differentiates between single trees (show details) and clusters (zoom in). The +2 zoom increment provides good progressive drilling.


297-309: LGTM!

Clean use of Consumer<WalletProvider> to reactively switch between map content and wallet connection prompt.


311-363: LGTM!

Well-structured map setup with proper onMapReady callback, gesture-only position change handling, and conditional marker layers. The OpenStreetMap tile layer includes appropriate userAgentPackageName.


422-526: LGTM!

Comprehensive overlay system with appropriate conditional rendering and position adjustments based on panel visibility.


528-544: LGTM!

The marker builder correctly differentiates between single trees and clusters. The force unwrap at line 540 is safe due to the isSingle guard.


546-614: LGTM!

Good visual differentiation for tree status (alive/deceased), selection state, and cluster density (green/orange/red based on count).


739-943: LGTM!

Comprehensive tree details panel with proper null handling, safe substring operation for geohash display (line 906-908), and clean navigation to full details page.


970-1059: LGTM!

Wallet connection prompt with proper async handling, mounted checks, and user-friendly error feedback via SnackBar.


1061-1065: LGTM!

Proper disposal of MapController to prevent memory leaks.

lib/widgets/map_widgets/map_filter_widget.dart (3)

5-60: LGTM!

Excellent use of the sentinel pattern to distinguish "not provided" from "explicitly null" in copyWith. This allows proper clearing of nullable filter fields.


100-322: LGTM!

Well-structured expandable filter panel with proper mutual exclusivity for status filters and clean species dropdown implementation.


324-437: LGTM!

QuickFilterBar provides intuitive one-tap toggles. The copyWith sentinel pattern correctly handles explicit null values to clear filters.

lib/widgets/map_widgets/tree_heatmap_layer.dart (3)

6-48: LGTM!

Clean heatmap layer with appropriate zoom-based visibility threshold and dynamic radius scaling.


119-171: LGTM!

Good alternative density visualization with appropriate size scaling and IgnorePointer to prevent blocking map interactions.


173-234: LGTM!

Simple and effective legend widget. The white background ensures visibility against the map.

lib/services/tree_map_service.dart (5)

60-77: LGTM!

Clean cluster model with convenient accessors for count, total tree count, and single-tree detection.


79-120: LGTM!

Well-designed caching strategy with O(1) duplicate detection via parallel Set<int> tracking. The singleton pattern is appropriate for a shared service.


122-180: LGTM!

Efficient bounds-based fetching with cache-first strategy and proper bounds filtering of results.


182-230: LGTM!

Proper pagination handling with safe substring operation at lines 207-209 using clamp to prevent index-out-of-bounds.


269-306: LGTM!

Clean clustering logic with proper handling of short/empty geohashes and correct center point calculation via coordinate averaging.

Tree is alive when death == 0 (not set) or death >= now (future timestamp).
Previous logic incorrectly used > instead of >= for comparison.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
lib/services/tree_map_service.dart (2)

234-269: Consider optimization for the nested loop.

The nested loop (lines 252-264) iterates over all trees for each requested geohash, resulting in O(n×m) complexity where n = number of trees and m = number of geohashes. While the comment acknowledges this is a temporary solution pending backend indexing, consider optimizing by:

  1. Building a prefix tree (trie) of geohashes for O(log n) lookups
  2. Or, iterating trees once and checking all prefixes per tree

Current approach:

for (final tree in _allTrees) {
  for (final geohash in geohashSet) {
    if (tree.geoHash.startsWith(geohash) || ...) {
      // O(n×m)
    }
  }
}

Alternative (O(n) with single pass):

for (final tree in _allTrees) {
  final encodedGeohash = _geohashService.encode(tree.latitude, tree.longitude);
  for (final geohash in geohashSet) {
    final len = geohash.length;
    if ((tree.geoHash.length >= len && tree.geoHash.substring(0, len) == geohash) ||
        (encodedGeohash.length >= len && encodedGeohash.substring(0, len) == geohash)) {
      _addTreeToCache(geohash, tree);
      break; // Early exit once matched
    }
  }
}

272-308: Well-implemented clustering with safe string handling.

The clustering logic correctly transitions from individual markers at high zoom (>= 15) to geohash-based clusters at lower zoom. The safe substring handling (line 289-291) prevents index errors.

Optional enhancement: Consider weighting cluster centers by numberOfTrees instead of treating each MapTreeData equally:

final totalWeight = clusterTrees.fold(0, (sum, t) => sum + t.numberOfTrees);
final centerLat = clusterTrees.fold(0.0, (sum, t) => sum + t.latitude * t.numberOfTrees) / totalWeight;
final centerLng = clusterTrees.fold(0.0, (sum, t) => sum + t.longitude * t.numberOfTrees) / totalWeight;

This would give more visual weight to NFTs representing multiple trees.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d04da04 and 06a3f9b.

📒 Files selected for processing (1)
  • lib/services/tree_map_service.dart (1 hunks)
🔇 Additional comments (5)
lib/services/tree_map_service.dart (5)

62-79: LGTM!

The TreeCluster class is well-designed with convenient getters. The totalTreeCount correctly sums the numberOfTrees field from all trees in the cluster, accounting for NFTs that may represent multiple physical trees.


81-122: Well-designed caching strategy.

The dual-map approach (_treeCache for data, _treeCacheIds for O(1) duplicate detection) is an excellent optimization. The singleton pattern is correctly implemented, and the clearCache method properly resets all state.


125-182: Robust bounds-based fetching with defensive filtering.

The implementation correctly:

  1. Calculates optimal geohash precision from zoom
  2. Reuses cached data
  3. Fetches missing geohashes
  4. Applies a final bounds filter (lines 172-177) to ensure accuracy despite geohash approximations

The defensive bounds filtering at the end is especially good practice.


185-232: Solid paginated fetching with safe string handling.

The implementation properly handles pagination and includes defensive substring handling (lines 208-212) with .clamp(1, tree.geoHash.length) to prevent index errors. The cache population per geohash with O(1) duplicate checking is efficient.


35-55: The isAlive logic is correct and semantically sound.

The condition death == 0 || death >= now properly handles both unset death values (never died) and scheduled death timestamps (future dates). The blockchain contract supports future death timestamps, treating them as trees that will die at a specified time but are currently alive. This interpretation is confirmed by consistent usage across the codebase (user_nfts_widget displays "Deceased" only when death < now, confirming that death >= now means alive).

One minor inconsistency exists: recent_trees_widget.dart uses death > now instead of death >= now, which differs on the boundary case when death == now. Consider aligning this to use >= for consistency.

- Split _convertCoordinate into _convertLatitude (-90 offset) and
  _convertLongitude (-180 offset) for correct decoding
- Use neighborGeohashes to pre-filter trees before distance calculations
  for better performance in getTreesNearLocation()
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
lib/services/tree_map_service.dart (1)

108-110: Consider returning an unmodifiable view for allTrees (avoid external mutation of service state).
Not required, but returning _allTrees directly makes it easy for callers to mutate internal state accidentally.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06a3f9b and e1833f5.

📒 Files selected for processing (1)
  • lib/services/tree_map_service.dart (1 hunks)

- Add type conversion helpers (_asInt, _asString) for contract data
- Handle both plantingDate and planting field names
- Remove _hasMore gate in fetchTreesInBounds for proper cache population
- Fix clamp() to int type mismatch for substring
- Return unmodifiable list from allTrees getter
- Include centerGeohash in nearby prefilter
- Cache distances to avoid recalculating during sort
- Fix misleading O(n) complexity comment
@kartikeyg0104
Copy link
Author

@Zahnentferner @chandansgowda @bhavik-mangla @a-singh09 Please review this pull request and if changes required please let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dynamic map that renders nearby planted trees

1 participant