Skip to content

Conversation

@parthraninga
Copy link

@parthraninga parthraninga commented Dec 13, 2025

Screen.Recording.2025-12-14.at.1.13.29.AM.mov

Closes#24
(Created stage environment locally to test and have o1 optimality)
This pull request introduces a new "Nearby Trees" map feature, improves geospatial clustering and search for trees, and fixes several coordinate calculation bugs. The main changes include adding a map page to display nearby trees, implementing geohash-based spatial utilities and clustering logic, updating UI components to link to the new map, and correcting longitude conversions throughout the app.

Nearby Trees Map Feature & Navigation:

  • Added a new NearbyTreesMapPage and integrated it into the app's navigation using GoRouter and manual navigation from relevant pages (main.dart, trees_page.dart, and recent_trees_widget.dart). This allows users to view trees near their location on a map. [1] [2] [3] [4] [5]

Geospatial Clustering and Utilities:

  • Introduced tree_clustering_service.dart to efficiently group trees for map display based on proximity and zoom level, using geohash-based clustering.
  • Added geohash_utils.dart providing geohash encoding/decoding, neighbor search, optimal precision calculation, and Haversine distance functions for spatial queries.

Blockchain Spatial Search:

  • Implemented getNearbyTrees in tree_nft_contract_read_services.dart to fetch and filter trees within a radius of a given location, using geohash coverage and distance calculations. [1] [2]

Coordinate Conversion Bug Fixes:

  • Fixed longitude conversion errors in several places, changing subtraction from 180.0 to 90.0 to correctly map contract coordinates to real-world longitude. This affects tree detail displays and user NFT widgets. [1] [2] [3]

UI/UX Improvements:

  • Updated the tree verification modal's hint text color for better readability by switching to a standard grey shade.

These changes collectively provide users with a more accurate and interactive way to explore trees near them, both visually and through improved backend spatial search.


Nearby Trees Map Integration

  • Added NearbyTreesMapPage and navigation routes/buttons to access the map from multiple locations in the app. [1] [2] [3] [4] [5]

Geospatial Clustering and Utilities

  • Implemented TreeClusteringService for clustering trees by proximity and zoom, and GeohashUtils for geohash encoding, decoding, neighbor search, and distance calculation. [1] [2]

Blockchain Spatial Search

  • Added getNearbyTrees to efficiently fetch and filter trees within a radius using geohash coverage and Haversine distance. [1] [2]

Coordinate Conversion Bug Fixes

  • Corrected longitude conversion from -180.0 to -90.0 in tree details and user NFT widgets for accurate location display. [1] [2] [3]

UI/UX Improvements

Summary by CodeRabbit

  • New Features
    • Interactive map page displaying nearby planted trees with your current location
    • "Map" button in All Trees view for quick navigation to the tree map
    • Location-based tree discovery showing trees sorted by proximity
    • Map controls including recenter and refresh buttons, tree count badge, and tree detail information

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 13, 2025

Walkthrough

This pull request introduces a new "nearby trees map" feature that displays planted trees on an interactive map with location-based filtering. It adds routing configuration, a new map page widget, geohash-based utility services for spatial queries and clustering, and integrates these components with existing tree data retrieval, while also adjusting coordinate transformations across multiple views.

Changes

Cohort / File(s) Summary
Routing & Navigation
lib/main.dart, lib/pages/trees_page.dart, lib/widgets/nft_display_utils/recent_trees_widget.dart
Added GoRoute for /nearby-trees path, created Map button in trees page header with navigation, and updated recent trees widget to navigate to the new route instead of showing a "coming soon" message.
New Map Feature
lib/pages/nearby_trees_map_page.dart
New StatefulWidget displaying nearby trees on an interactive FlutterMap with location retrieval, marker rendering for trees and user location, loading/error/success state handling, and a bottom sheet showing tree details with navigation to full details.
Spatial Utilities
lib/utils/geohash_utils.dart, lib/services/tree_clustering_service.dart
Added GeohashUtils class providing encode/decode, neighbor queries, distance calculations, and coverage geohash generation. Added TreeClusteringService with clustering logic that groups trees by geohash at lower zoom levels and displays individual trees at zoom ≥14.
Data Retrieval Service
lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart
Added static method getNearbyTrees that fetches recent trees, filters by radius using geohash coverage and distance calculation, appends distance metrics, and returns results sorted by proximity.
Coordinate Adjustments
lib/pages/tree_details_page.dart, lib/widgets/nft_display_utils/user_nfts_widget.dart
Modified longitude offset calculation from subtracting 180 to subtracting 90 (shifting displayed longitude by +90 degrees). Updated verification modal text color from theme-derived to fixed grey shade.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as NearbyTreesMapPage
    participant Location as Location Service
    participant Geohash as GeohashUtils
    participant Contract as ContractReadFunctions
    participant Cluster as TreeClusteringService
    participant Map as FlutterMap

    User->>UI: Navigate to nearby trees
    UI->>Location: Get user location
    Location-->>UI: Current coords (or default)
    
    rect rgb(220, 240, 220)
        Note over UI: Loading State
    end
    
    UI->>Geohash: getCoverageGeohashes(center, radius)
    Geohash-->>UI: Coverage geohashes
    
    UI->>Contract: getNearbyTrees(wallet, center, radius)
    Contract->>Contract: Compute coverage geohashes
    Contract->>Contract: Fetch recent trees (limit 50)
    Contract->>Geohash: calculateDistance for each tree
    Contract-->>UI: Filtered trees with distance metrics
    
    rect rgb(240, 220, 240)
        Note over UI: Success State
    end
    
    UI->>Cluster: clusterTrees(trees, zoomLevel)
    Cluster-->>UI: Clustered/individual markers
    
    UI->>Map: Render markers & user location
    Map-->>UI: Map displayed
    
    User->>Map: Tap tree marker
    Map->>UI: Show bottom sheet
    UI-->>User: Display tree details & navigation option
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–75 minutes

Areas requiring extra attention:

  • lib/utils/geohash_utils.dart: Geohash encoding/decoding algorithm correctness, neighbor generation logic, and distance calculations (Haversine formula)
  • lib/services/tree_clustering_service.dart: Clustering logic correctness at different zoom levels, bounds calculation, and geohash precision selection
  • lib/pages/nearby_trees_map_page.dart: State management during async operations, proper dispose/cleanup, error handling, and map marker tap interactions
  • lib/utils/services/contract_functions/.../tree_nft_contract_read_services.dart: Coordinate conversion accuracy, filtering logic for radius-based proximity, and distance metric computation
  • Coordinate transformations: Verify the longitude offset change (−90 vs. −180) is applied consistently across all affected files (tree_details_page, user_nfts_widget, new utilities)

Possibly related PRs

  • NFT Display page #13: Modifies tree details page and static map coordinate handling, overlapping with coordinate transformation changes in this PR.

Suggested reviewers

  • ceilican
  • Zahnentferner
  • bhavik-mangla

Poem

🐰 Hop, hop! A map appears so fine,
With geohashes marking every tree line,
Cluster and filter by distance near,
Interactive markers make locations clear,
The woodland quest now finds its way,
With leafy pins to guide the day! 🌍🌳

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing a dynamic map feature to display nearby planted trees, which aligns with the substantial changes across the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 10

🧹 Nitpick comments (10)
lib/main.dart (1)

139-145: Prefer a route constant / named route usage for consistency.

Since most routes use RouteConstants, consider adding something like RouteConstants.nearbyTreesPath (and maybe a RouteConstants.nearbyTrees name) to avoid string drift across the app.

lib/widgets/nft_display_utils/recent_trees_widget.dart (1)

374-376: Good: button now navigates instead of a placeholder.

Small improvement: use a route constant / pushNamed('nearby_trees') to avoid repeating '/nearby-trees' literals in multiple places.

lib/pages/tree_details_page.dart (1)

246-253: Same coordinate conversion risk in displayed longitude chip.

If - 90.0 is correct, consider replacing these inline conversions with a shared helper to prevent future mismatches.

lib/pages/trees_page.dart (1)

56-85: Use go_router navigation here (avoid mixing Navigator.push and context.push).

This app already has a /nearby-trees route; using Navigator.push(MaterialPageRoute(...)) bypasses routing/deep-link consistency and can create two parallel navigation styles.

- ElevatedButton.icon(
-   onPressed: () {
-     Navigator.push(
-       context,
-       MaterialPageRoute(
-         builder: (context) => const NearbyTreesMapPage(),
-       ),
-     );
-   },
+ ElevatedButton.icon(
+   onPressed: () {
+     context.push('/nearby-trees');
+   },
    icon: const Icon(Icons.map, size: 20),
    label: const Text(
      'Map',
      style: TextStyle(fontWeight: FontWeight.bold),
    ),
    ...
  ),
lib/pages/nearby_trees_map_page.dart (1)

115-151: Consolidate duplicate coordinate conversion logic into a shared utility.

The _convertCoordinate method is duplicated across multiple files (nearby_trees_map_page.dart, tree_clustering_service.dart, recent_trees_widget.dart, planting_proposals_tab.dart). Move this fixed-point-to-degrees conversion to a centralized helper in lib/utils/ to reduce maintenance burden and ensure consistency.

Note: The as int casts are safe in current implementation since the contract read services already convert BigInt to int before returning tree data. However, centralizing the conversion logic eliminates potential future brittleness and makes the data transformation more explicit.

lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart (2)

6-6: Unused import.

mock_trees.dart is imported but not used anywhere in this file. This appears to be dead code that should be removed.

-import 'package:tree_planting_protocol/utils/mock_data/mock_trees.dart';

409-416: Coverage geohashes are computed but never used for filtering.

coverageGeohashes is calculated and logged but not used to filter trees. All trees from getRecentTreesPaginated are iterated and filtered by distance only. While the TODO comment acknowledges this, the geohash calculation is currently wasted computation.

Consider either:

  1. Removing the geohash calculation until the contract supports geohash queries
  2. Using geohashes for client-side pre-filtering before distance calculation
lib/services/tree_clustering_service.dart (2)

139-142: Duplicated coordinate conversion logic.

_convertCoordinate is duplicated here and in tree_nft_contract_read_services.dart. Consider extracting this to a shared utility (e.g., in GeohashUtils or a dedicated coordinate converter) to ensure consistency and avoid maintenance burden.


110-117: Dead code: return value for zoom >= 14 is unreachable.

Line 116 (return 0.2) is never reached because clusterTrees returns early at line 39 when zoomLevel >= 14. The function _getClusterDistance is only called when zoomLevel < 14.

   static double _getClusterDistance(double zoom) {
     if (zoom <= 5) return 100.0; // 100km clusters
     if (zoom <= 7) return 50.0;  // 50km clusters
     if (zoom <= 9) return 20.0;  // 20km clusters
     if (zoom <= 11) return 5.0;  // 5km clusters
-    if (zoom <= 13) return 1.0;  // 1km clusters
-    return 0.2; // 200m clusters
+    return 1.0;  // 1km clusters (zoom 12-13)
   }
lib/utils/geohash_utils.dart (1)

146-167: Documentation mismatch with implementation.

The comments at lines 147-156 show different precision values (e.g., "1: ±2500 km") than what the thresholds actually check (e.g., radiusKm > 630 returns 1). The comments appear to describe cell dimensions while the thresholds are tuned for coverage. Consider aligning the documentation with the actual selection logic.

📜 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 fb36612.

📒 Files selected for processing (9)
  • lib/main.dart (2 hunks)
  • lib/pages/nearby_trees_map_page.dart (1 hunks)
  • lib/pages/tree_details_page.dart (3 hunks)
  • lib/pages/trees_page.dart (2 hunks)
  • lib/services/tree_clustering_service.dart (1 hunks)
  • lib/utils/geohash_utils.dart (1 hunks)
  • lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart (2 hunks)
  • lib/widgets/nft_display_utils/recent_trees_widget.dart (1 hunks)
  • lib/widgets/nft_display_utils/user_nfts_widget.dart (1 hunks)
🔇 Additional comments (7)
lib/main.dart (1)

19-20: LGTM: route page import wired cleanly.

lib/pages/trees_page.dart (1)

9-10: LGTM: map page import added.

lib/pages/nearby_trees_map_page.dart (1)

42-46: Confirm MapController.dispose() is valid for flutter_map 8.1.1.

If MapController doesn’t expose dispose() in your pinned version, this won’t compile (or you may be double-disposing via inherited behavior). Please verify against the flutter_map 8.1.1 API.

lib/services/tree_clustering_service.dart (1)

31-50: LGTM on high-zoom bypass logic.

The early return for zoomLevel >= 14 correctly creates individual tree clusters without grouping, which is appropriate for high zoom levels where individual markers should be visible.

lib/utils/geohash_utils.dart (3)

109-122: Potential edge case at geographic boundaries.

getNeighbors calls _getNeighbor recursively for diagonal neighbors (e.g., line 117: _getNeighbor(_getNeighbor(geohash, 'top'), 'right')). If the inner _getNeighbor returns an empty string (which can happen at extreme boundaries when parent.isEmpty at line 217), the outer call will also return empty string.

This could result in empty strings in the returned neighbor list for geohashes at the edges of the coordinate space. Consider filtering out empty results or documenting this behavior.


172-190: LGTM on Haversine distance calculation.

The Haversine formula implementation is correct and properly handles the spherical geometry for calculating distances between two geographic points.


22-63: LGTM on geohash encoding.

The encoding implementation correctly follows the standard geohash algorithm with alternating longitude/latitude bits and base32 character encoding.

Comment on lines +32 to +35
// Default location (fallback if GPS fails)
static const double _defaultLat = 28.9845; // Example: Roorkee, India
static const double _defaultLng = 77.8956;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Default location set to a specific city (India) is likely surprising in production.

If location permissions fail, consider defaulting to a neutral world view (zoomed out), last-known location, or prompting the user instead of jumping to a specific city.

🤖 Prompt for AI Agents
In lib/pages/nearby_trees_map_page.dart around lines 32 to 35, the current
hardcoded default coordinates point to a specific city (Roorkee, India); remove
this surprising production behavior by replacing the fixed
_defaultLat/_defaultLng with a neutral fallback strategy: try to use a
stored/lastKnown location (if available) and only if that is absent fall back to
a world-center view (e.g., latitude 0, longitude 0) with a low zoom level, or
surface a prompt to the user asking to allow location or pick a location; update
the map initialization logic to consult lastKnownLocation first, then use the
neutral world view or show a permission/location prompt, and remove or deprecate
the city-specific constants.

Comment on lines +48 to +107
Future<void> _loadNearbyTrees() async {
setState(() {
_isLoading = true;
_hasError = false;
_errorMessage = null;
});

try {
// Try to get user's current location, but don't fail if it's unavailable
try {
final locationInfo = await _locationService.getCurrentLocation();
_userLat = locationInfo.latitude;
_userLng = locationInfo.longitude;
logger.i("User location: $_userLat, $_userLng");
} catch (locationError) {
// Location failed, use default location
logger.w("Could not get user location, using default: $locationError");
_userLat = _defaultLat;
_userLng = _defaultLng;
}

// Check if widget is still mounted
if (!mounted) return;

// Fetch nearby trees using either actual or default location
final walletProvider =
Provider.of<WalletProvider>(context, listen: false);

final result = await ContractReadFunctions.getNearbyTrees(
walletProvider: walletProvider,
centerLat: _userLat!,
centerLng: _userLng!,
radiusKm: 10.0, // 10km radius
);

if (result.success && result.data != null) {
final trees = result.data['trees'] as List<Map<String, dynamic>>;

setState(() {
_nearbyTrees = trees;
_isLoading = false;
});

logger.i("Loaded ${trees.length} nearby trees");
} else {
throw Exception(result.errorMessage ?? 'Failed to load trees');
}
} catch (e) {
logger.e("Error loading nearby trees: $e");

// Use fallback location if not already set
setState(() {
_userLat ??= _defaultLat;
_userLng ??= _defaultLng;
_hasError = true;
_errorMessage = e.toString();
_isLoading = false;
});
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard setState after async awaits + avoid unsafe casts from result.data.

  1. You check mounted before the contract call, but the widget can still unmount while awaiting getNearbyTrees().
  2. result.data['trees'] as List<Map<String, dynamic>> is a common runtime crash if the value is List<dynamic>.
   final result = await ContractReadFunctions.getNearbyTrees(
     walletProvider: walletProvider,
     centerLat: _userLat!,
     centerLng: _userLng!,
     radiusKm: 10.0,
   );
+  if (!mounted) return;

   if (result.success && result.data != null) {
-    final trees = result.data['trees'] as List<Map<String, dynamic>>;
+    final rawTrees = (result.data['trees'] as List?) ?? const [];
+    final trees = List<Map<String, dynamic>>.from(rawTrees);

     setState(() {
       _nearbyTrees = trees;
       _isLoading = false;
     });
   } else {
     throw Exception(result.errorMessage ?? 'Failed to load trees');
   }
...
 } catch (e) {
   logger.e("Error loading nearby trees: $e");
-  setState(() {
+  if (!mounted) return;
+  setState(() {
     _userLat ??= _defaultLat;
     _userLng ??= _defaultLng;
     _hasError = true;
     _errorMessage = e.toString();
     _isLoading = false;
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<void> _loadNearbyTrees() async {
setState(() {
_isLoading = true;
_hasError = false;
_errorMessage = null;
});
try {
// Try to get user's current location, but don't fail if it's unavailable
try {
final locationInfo = await _locationService.getCurrentLocation();
_userLat = locationInfo.latitude;
_userLng = locationInfo.longitude;
logger.i("User location: $_userLat, $_userLng");
} catch (locationError) {
// Location failed, use default location
logger.w("Could not get user location, using default: $locationError");
_userLat = _defaultLat;
_userLng = _defaultLng;
}
// Check if widget is still mounted
if (!mounted) return;
// Fetch nearby trees using either actual or default location
final walletProvider =
Provider.of<WalletProvider>(context, listen: false);
final result = await ContractReadFunctions.getNearbyTrees(
walletProvider: walletProvider,
centerLat: _userLat!,
centerLng: _userLng!,
radiusKm: 10.0, // 10km radius
);
if (result.success && result.data != null) {
final trees = result.data['trees'] as List<Map<String, dynamic>>;
setState(() {
_nearbyTrees = trees;
_isLoading = false;
});
logger.i("Loaded ${trees.length} nearby trees");
} else {
throw Exception(result.errorMessage ?? 'Failed to load trees');
}
} catch (e) {
logger.e("Error loading nearby trees: $e");
// Use fallback location if not already set
setState(() {
_userLat ??= _defaultLat;
_userLng ??= _defaultLng;
_hasError = true;
_errorMessage = e.toString();
_isLoading = false;
});
}
}
Future<void> _loadNearbyTrees() async {
setState(() {
_isLoading = true;
_hasError = false;
_errorMessage = null;
});
try {
// Try to get user's current location, but don't fail if it's unavailable
try {
final locationInfo = await _locationService.getCurrentLocation();
_userLat = locationInfo.latitude;
_userLng = locationInfo.longitude;
logger.i("User location: $_userLat, $_userLng");
} catch (locationError) {
// Location failed, use default location
logger.w("Could not get user location, using default: $locationError");
_userLat = _defaultLat;
_userLng = _defaultLng;
}
// Check if widget is still mounted
if (!mounted) return;
// Fetch nearby trees using either actual or default location
final walletProvider =
Provider.of<WalletProvider>(context, listen: false);
final result = await ContractReadFunctions.getNearbyTrees(
walletProvider: walletProvider,
centerLat: _userLat!,
centerLng: _userLng!,
radiusKm: 10.0, // 10km radius
);
if (!mounted) return;
if (result.success && result.data != null) {
final rawTrees = (result.data['trees'] as List?) ?? const [];
final trees = List<Map<String, dynamic>>.from(rawTrees);
setState(() {
_nearbyTrees = trees;
_isLoading = false;
});
logger.i("Loaded ${trees.length} nearby trees");
} else {
throw Exception(result.errorMessage ?? 'Failed to load trees');
}
} catch (e) {
logger.e("Error loading nearby trees: $e");
// Use fallback location if not already set
if (!mounted) return;
setState(() {
_userLat ??= _defaultLat;
_userLng ??= _defaultLng;
_hasError = true;
_errorMessage = e.toString();
_isLoading = false;
});
}
}

Comment on lines +175 to +256
void _showTreeDetails(Map<String, dynamic> tree) {
final lat = _convertCoordinate(tree['latitude'] as int);
final lng = _convertCoordinate(tree['longitude'] as int);
final plantingDate = DateTime.fromMillisecondsSinceEpoch(
(tree['planting'] as int) * 1000,
);

showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: getThemeColors(context)['background'],
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
border: Border(
top: BorderSide(
color: getThemeColors(context)['border']!,
width: 2,
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Drag handle
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(2),
),
),
),

// Tree details
Text(
tree['species'] ?? 'Unknown Species',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: getThemeColors(context)['textPrimary'],
),
),
const SizedBox(height: 12),

_buildDetailRow(Icons.tag, 'ID #${tree['id']}'),
_buildDetailRow(Icons.calendar_today,
'Planted: ${plantingDate.year}-${plantingDate.month}-${plantingDate.day}'),
_buildDetailRow(Icons.location_on,
'${lat.toStringAsFixed(4)}, ${lng.toStringAsFixed(4)}'),
_buildDetailRow(Icons.favorite, '${tree['careCount']} care events'),
_buildDetailRow(Icons.photo,
'${(tree['photos'] as List).length} photos'),

const SizedBox(height: 16),

// View full details button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
context.push('/trees/${tree['id']}');
},
icon: const Icon(Icons.info_outline),
label: const Text('View Full Details'),
style: ElevatedButton.styleFrom(
backgroundColor: getThemeColors(context)['primary'],
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
),
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Bottom sheet: null-safe photo count + consider centering map on selected marker.

  • tree['photos'] might be null/missing; (tree['photos'] as List).length can crash.
  • UX: after selecting a tree, consider moving/animating the map to that marker so users see what they tapped (optional).
- _buildDetailRow(Icons.photo,
-     '${(tree['photos'] as List).length} photos'),
+ final photos = (tree['photos'] as List?) ?? const [];
+ _buildDetailRow(Icons.photo, '${photos.length} photos'),

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In lib/pages/nearby_trees_map_page.dart around lines 175 to 256, the
bottom-sheet reads '(tree['photos'] as List).length' which will throw if
'photos' is null or missing; change to a null-safe count like '(tree['photos']
as List?)?.length ?? 0' (or compute an int photosCount first) and display that
variable instead; additionally, optionally animate/center the map on the
selected tree when showing details by using your map controller (e.g., call the
controller's animate/animateCamera method to the converted lat/lng before or
after showing the sheet) so the map moves to the selected marker.

Comment on lines +289 to +313
if (!_isLoading && !_hasError)
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: LatLng(_userLat ?? _defaultLat, _userLng ?? _defaultLng),
initialZoom: 13.0,
minZoom: 3.0,
maxZoom: 18.0,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all,
),
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.stability.nexus.tree_planting_protocol',
),
MarkerLayer(
markers: [
if (_buildUserMarker() != null) _buildUserMarker()!,
..._buildTreeMarkers(),
],
),
],
),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the full nearby_trees_map_page.dart file to understand the context
wc -l lib/pages/nearby_trees_map_page.dart

Repository: StabilityNexus/Treee

Length of output: 104


🏁 Script executed:

# Search for any attribution-related code in the entire codebase
rg -i "attribution|openstreetmap|© |license" --type dart

Repository: StabilityNexus/Treee

Length of output: 563


🏁 Script executed:

# Check if flutter_map has any attribution layer or control being used
rg -i "attributionlayer|attributioncontrol" --type dart

Repository: StabilityNexus/Treee

Length of output: 46


🏁 Script executed:

# Look for TileLayer usage to see if there are other instances with attribution
rg -B5 -A5 "TileLayer" --type dart

Repository: StabilityNexus/Treee

Length of output: 3828


🌐 Web query:

flutter_map attribution layer OpenStreetMap tiles

💡 Result:

Use flutter_map’s built-in attribution widgets (SimpleAttributionWidget or RichAttributionWidget) and put them into the map’s nonRotatedChildren so the OSM credit is always visible. Also set a user-agent/ package name on TileLayer when using public OSM tiles.

Minimal example:

FlutterMap(
  options: MapOptions(center: LatLng(0,0), zoom: 2),
  children: [
    TileLayer(
      urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
      subdomains: ['a','b','c'],
      userAgentPackageName: 'com.yourcompany.yourapp', // set to identify your client
    ),
    // static atop the map so attribution doesn't rotate
    SimpleAttributionWidget(
      source: Text('© OpenStreetMap contributors'),
      alignment: Alignment.bottomRight,
    ),
  ],
)

Or use RichAttributionWidget to include logos and clickable links:

RichAttributionWidget(
  attributions: [
    TextSourceAttribution(
      'OpenStreetMap contributors',
      onTap: () => launchUrl(Uri.parse('https://www.openstreetmap.org/copyright')),
    ),
  ],
)

Notes

  • Attribution must be visible and comply with the tile server ToS (OSM requires “© OpenStreetMap contributors”). [1][2]
  • For public OSM tiles set TileLayer.userAgentPackageName to identify your app (and follow any additional tile-policy requirements). [1][3]

Sources: flutter_map docs (Attribution Layer, Tile/OSM guidance). [1][2][3]


🌐 Web query:

OpenStreetMap license attribution requirements tile usage

💡 Result:

Summary — what you must do when using OpenStreetMap tiles

  • Show visible attribution on the map (legible, near the map). Typical text: "© OpenStreetMap contributors" (you may shorten to "© OpenStreetMap"). Provide a link to the OSM copyright/license page (e.g. www.openstreetmap.org/copyright) or otherwise make the licence (ODbL) discoverable. [1][2]

  • Follow the OSM Tile Usage Policy for tile.openstreetmap.org:

    • Use the correct tile URL (https://tile.openstreetmap.org/{z}/{x}/{y}.png).
    • Don’t bulk-download or scrape tiles; cache according to HTTP headers (or at least 7 days).
    • Send a clear User‑Agent (identify your app and contact).
    • Ensure a valid HTTP Referer when used from web pages.
    • Don’t hide attribution behind toggles or off-screen. [1]
  • If you create a produced work (render your own tiles from OSM data) you must still attribute OSM and comply with the ODbL requirements (including making the licence clear and meeting share‑alike requirements where applicable). The OSM Attribution Guidelines give safe‑harbour placement and wording guidance. [2][3]

Useful references

  • OSM Tile Usage Policy (tile.openstreetmap.org). [1]
  • OSM Licence / Attribution Guidelines (ODbL attribution rules & examples). [2]
  • OSM Legal FAQ (credit text and where to place it). [3]

If you want, I can draft an attribution snippet and show example placement for a web map or a mobile app.


Add visible OpenStreetMap attribution (legal requirement).

OSM tile usage requires visible attribution on the map ("© OpenStreetMap contributors" plus a link to https://www.openstreetmap.org/copyright). Use flutter_map's SimpleAttributionWidget or RichAttributionWidget in the FlutterMap's children to display it prominently (typically bottom-right, never hidden or toggled off). This applies to all three map instances in the codebase: nearby_trees_map_page.dart, flutter_map_widget.dart, and static_map_display_widget.dart.

🤖 Prompt for AI Agents
In lib/pages/nearby_trees_map_page.dart around lines 289 to 313, the FlutterMap
instance is missing the required visible OpenStreetMap attribution; add a
SimpleAttributionWidget (or RichAttributionWidget) to the FlutterMap children
with the text "© OpenStreetMap contributors" and a link to
https://www.openstreetmap.org/copyright, positioned bottomRight (and not
hidden/toggleable). Apply the same change to the other two map instances in
lib/widgets/flutter_map_widget.dart and
lib/widgets/static_map_display_widget.dart so all maps display the mandatory OSM
attribution visibly by default.

Comment on lines +315 to +362
// Loading state
if (_isLoading)
Container(
color: Colors.white,
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading nearby trees...'),
],
),
),
),

// Error state
if (_hasError)
Container(
color: Colors.white,
padding: const EdgeInsets.all(20),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 60, color: Colors.red[300]),
const SizedBox(height: 16),
const Text(
'Failed to load location',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
_errorMessage ?? 'Unknown error',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 20),
ElevatedButton.icon(
onPressed: _loadNearbyTrees,
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
),
],
),
),
),

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Loading/error overlays should respect theme and correct messaging.

  • Hardcoding Colors.white will look wrong in dark mode.
  • Error header says “Failed to load location” but failures can be from contract fetch as well—rename to something like “Failed to load nearby trees”.
🤖 Prompt for AI Agents
In lib/pages/nearby_trees_map_page.dart around lines 315 to 362, the
loading/error overlay currently hardcodes Colors.white (which breaks dark mode)
and the error header text reads "Failed to load location" despite errors
possibly coming from other sources; change the containers to use
Theme.of(context).scaffoldBackgroundColor or a themed color (or wrap with
Material with color from theme) so overlays respect light/dark mode, and update
the error header text to a generic message like "Failed to load nearby trees"
while keeping the existing retry button and error message display.

Comment on lines 910 to 913
hintStyle: TextStyle(
color: getThemeColors(context)['textSecondary'],
color: Colors.grey.shade600,
),
border: OutlineInputBorder(
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hint text color: fixed grey may reduce dark-mode readability.

If this was to improve contrast on light theme, consider deriving from theme (e.g., textSecondary with adjusted opacity) so it remains legible in dark mode too.

🤖 Prompt for AI Agents
In lib/pages/tree_details_page.dart around lines 910 to 913, the hintStyle uses
a fixed Colors.grey.shade600 which can be too light in dark mode; change it to
derive the color from the current theme (use Theme.of(context) — e.g., text
theme or colorScheme onSurface/onBackground with an appropriate opacity) so the
hint color adapts to light/dark modes and maintains contrast; replace the
hardcoded color with a theme-derived color expression and adjust opacity if
needed.

Comment on lines +466 to +477
// Check if tree is within radius
if (distance <= radiusKm) {
// Add distance to tree data
final treeWithDistance = Map<String, dynamic>.from(tree);
treeWithDistance['distanceKm'] = distance;
treeWithDistance['distanceMeters'] = (distance * 1000).round();
nearbyTrees.add(treeWithDistance);
}

// Stop if we've found enough trees
if (nearbyTrees.length >= limit) break;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Early break may return suboptimal results.

The loop breaks early when nearbyTrees.length >= limit, but sorting by distance happens afterward (lines 480-484). This means if the first 100 trees found happen to be farther away, closer trees appearing later in allTrees will be missed.

Apply this diff to collect all nearby trees first, then sort and limit:

         // Check if tree is within radius
         if (distance <= radiusKm) {
           // Add distance to tree data
           final treeWithDistance = Map<String, dynamic>.from(tree);
           treeWithDistance['distanceKm'] = distance;
           treeWithDistance['distanceMeters'] = (distance * 1000).round();
           nearbyTrees.add(treeWithDistance);
         }
-
-        // Stop if we've found enough trees
-        if (nearbyTrees.length >= limit) break;
       }

       // Step 4: Sort by distance (closest first)
       nearbyTrees.sort((a, b) {
         final distA = a['distanceKm'] as double;
         final distB = b['distanceKm'] as double;
         return distA.compareTo(distB);
       });

+      // Apply limit after sorting
+      final limitedTrees = nearbyTrees.take(limit).toList();
+
       logger.i("Found ${nearbyTrees.length} trees within ${radiusKm}km");

       return ContractReadResult.success(
         data: {
-          'trees': nearbyTrees,
-          'totalCount': nearbyTrees.length,
+          'trees': limitedTrees,
+          'totalCount': nearbyTrees.length, // Total found, not limited count

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart
around lines 466 to 477, the loop currently breaks as soon as nearbyTrees.length
>= limit which can skip closer trees found later; remove the early break so the
code continues scanning all trees and collects every tree within radius, then
after the loop sort nearbyTrees by distance (distanceKm) and apply the limit
(take the first N entries) so you return the true closest trees.

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

2 participants