-
Notifications
You must be signed in to change notification settings - Fork 19
feat: used web3.storage along with fallback to ipfs with same naming convention #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughThe PR implements a geohash-based nearby trees map feature with tree clustering, migrates IPFS storage from Pinata to Web3.Storage using Bearer token authentication, adds contract-level spatial query functionality, fixes coordinate system calculations (longitude offset 180→90), and updates routing and UI widgets accordingly. Changes
Sequence DiagramsequenceDiagram
participant User as User
participant App as NearbyTreesMapPage
participant Loc as LocationService
participant Contract as ContractReadFunctions
participant Geo as GeohashUtils
participant Cluster as TreeClusteringService
participant Map as MapController
User->>App: Open nearby trees map
App->>App: Initialize state
App->>Loc: Get user location
activate Loc
Loc-->>App: Return lat/lng or default
deactivate Loc
App->>Geo: getCoverageGeohashes(lat, lng, radius)
activate Geo
Geo-->>App: Return geohash list
deactivate Geo
App->>Contract: getNearbyTrees(center, radius, limit)
activate Contract
Contract->>Contract: Fetch trees via geohashes
Contract->>Geo: calculateDistance for filtering
Contract-->>App: Return nearby trees with distances
deactivate Contract
App->>Cluster: clusterTrees(trees, zoomLevel)
activate Cluster
Cluster-->>App: Return clustered/individual markers
deactivate Cluster
App->>Map: Set markers & center
activate Map
Map-->>App: Map rendered
deactivate Map
User->>App: Tap tree marker
App->>App: Show tree details modal
User->>App: Tap recenter or refresh
App->>App: Reload data and redraw
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Areas requiring extra attention:
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 12
🧹 Nitpick comments (9)
lib/pages/tree_details_page.dart (1)
911-911: Consider using theme-derived color instead of hardcoded value.The hint color changed from
getThemeColors(context)['textSecondary']toColors.grey.shade600. Hardcoding colors can cause:
- Inconsistent theming across the app
- Poor contrast in dark mode
- Accessibility concerns
Apply this diff to use the theme color:
hintStyle: TextStyle( - color: Colors.grey.shade600, + color: getThemeColors(context)['textSecondary'], ),lib/utils/geohash_utils.dart (1)
195-222: Add error handling for invalid geohash in _getNeighbor.The
_getNeighbormethod could throw index out of range exceptions if the geohash contains invalid characters that aren't found in the lookup tables.Add a try-catch block or validation:
static String _getNeighbor(String geohash, String direction) { if (geohash.isEmpty) return ''; + + try { final lastChar = geohash[geohash.length - 1]; + if (!_base32.contains(lastChar)) { + return geohash; // Return original if invalid + } final parent = geohash.substring(0, geohash.length - 1); final type = geohash.length % 2 == 0 ? 'even' : 'odd'; // ... rest of method + } catch (e) { + return geohash; // Fallback to original on error + } }lib/pages/trees_page.dart (1)
56-65: Inconsistent navigation pattern: usego_routerfor consistency.The existing "Mint NFT" button uses
context.push('/mint-nft')via go_router, but this new Map button usesNavigator.push. According to the AI summary, a route for/nearby-treesis registered inmain.dart. UsingNavigator.pushbypasses go_router, which can break deep linking, browser history (on web), and consistent navigation behavior.- onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const NearbyTreesMapPage(), - ), - ); - }, + onPressed: () { + context.push('/nearby-trees'); + },If using go_router, the direct import of
NearbyTreesMapPageon line 9 would also become unnecessary.lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart (2)
6-6: Unused import.The
mock_trees.dartimport is not used in this file.-import 'package:tree_planting_protocol/utils/mock_data/mock_trees.dart';
409-416: ComputedcoverageGeohashesare unused for filtering.The geohash coverage is calculated and logged but not actually used to filter trees. The current implementation fetches all recent trees and filters purely by distance. While the TODO comment acknowledges this limitation, the
coverageGeohashescomputation is currently dead code.Consider either:
- Removing the unused computation until contract-side filtering is implemented
- Using it for client-side pre-filtering before distance calculation (could improve performance for large datasets)
lib/utils/services/ipfs_services.dart (1)
23-30: Add error logging for failed uploads.When the upload fails (non-200 status), the function returns
nullwithout logging the error. This makes debugging difficult in production.if (response.statusCode == 200) { var jsonResponse = json.decode(await response.stream.bytesToString()); // Web3.Storage returns a CID in the response String cid = jsonResponse['cid']; return "https://w3s.link/ipfs/$cid"; } else { + // Log the error for debugging + final errorBody = await response.stream.bytesToString(); + // Consider using your logger: logger.e("IPFS upload failed: ${response.statusCode} - $errorBody"); return null; }lib/services/tree_clustering_service.dart (2)
139-142: Duplicated_convertCoordinatefunction.This helper is duplicated in
tree_nft_contract_read_services.dart. Consider extracting it to a shared utility (e.g., ingeohash_utils.dartor a coordinate utilities file) to follow DRY principles.Additionally, the same coordinate conversion concern applies here: using
-90.0offset may be incorrect for longitude values.
119-137: Bounds calculation is safe but could be more defensive.The force-unwraps on lines 134-135 (
south!,west!,north!,east!) are safe in the current usage since_calculateBoundsis only called whentreesInGroup.length >= minClusterSize(at least 2). However, if this method were called directly with an empty list in the future, it would throw.Consider adding a guard clause for robustness:
static LatLngBounds _calculateBounds(List<Map<String, dynamic>> trees) { + if (trees.isEmpty) { + return LatLngBounds(LatLng(0, 0), LatLng(0, 0)); + } double? north, south, east, west;lib/pages/nearby_trees_map_page.dart (1)
279-422: Theming/UX: avoid hard-coded white backgrounds; fix error copy; avoid double-building user marker
- Loading/error overlays use
Colors.white(Lines 318, 334) which will look wrong in dark mode; useTheme.of(context).scaffoldBackgroundColororgetThemeColors(context)['background'].- Error title says “Failed to load location” even when contract fetch fails (Lines 342-345).
_buildUserMarker()is called twice (Line 308) and does work; cache to a local variable inbuild.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
.env.stencil(1 hunks)README.md(1 hunks)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/utils/services/ipfs_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)lib/widgets/profile_widgets/profile_section_widget.dart(1 hunks)lib/widgets/profile_widgets/user_profile_viewer_widget.dart(1 hunks)
🔇 Additional comments (8)
README.md (1)
142-142: Documentation accurately reflects the storage migration.The update correctly documents the switch from generic IPFS to Web3.Storage/Storacha, aligning with the PR's objective to use a more cost-effective storage solution.
lib/widgets/profile_widgets/profile_section_widget.dart (1)
393-395: Good defensive programming for IPFS gateway fallback.The extension to support both
pinata.cloudandw3s.linkURLs ensures backward compatibility while accommodating the new Web3.Storage provider. The fallback toipfs.iogateway is appropriate for both services.lib/pages/tree_details_page.dart (1)
195-195: Verify coordinate conversion consistency with the smart contract.Both the map rendering and display text now use
- 90.0offset for longitude (previously 180.0). This matches the change inuser_nfts_widget.dart, suggesting a systematic correction across the codebase.Confirm this aligns with how coordinates are stored on-chain and that the map markers render at the correct locations.
Also applies to: 246-246
lib/main.dart (1)
19-19: Route integration is well-structured.The new
/nearby-treesroute follows the existing GoRouter pattern and properly integrates the NearbyTreesMapPage. The route naming and structure are consistent with other routes in the application.Also applies to: 139-145
lib/widgets/nft_display_utils/recent_trees_widget.dart (1)
375-375: Map button now properly navigates to the nearby trees feature.Good change replacing the placeholder SnackBar with actual navigation to
/nearby-trees, making the map functionality accessible to users.lib/utils/geohash_utils.dart (1)
1-223: Geohash implementation is correct and production-ready.All components have been verified:
- Binary subdivision algorithm for encode/decode is mathematically correct
- Neighbor and border lookup tables match the geohash specification exactly
- Haversine formula uses the standard Earth radius (6371 km) and correct implementation
- Precision-to-radius mapping provides appropriate geohash granularity for proximity searches
- Edge cases are properly handled (empty strings, boundary detection, null-safe operators)
The implementation will effectively support the nearby trees map feature with accurate spatial indexing and neighbor cell discovery.
lib/widgets/profile_widgets/user_profile_viewer_widget.dart (1)
169-174: LGTM!The expanded fallback logic correctly handles both legacy Pinata URLs and new Web3.Storage (
w3s.link) URLs by extracting the IPFS hash and falling back to theipfs.iogateway. This aligns with the migration to Web3.Storage while maintaining backward compatibility.lib/services/tree_clustering_service.dart (1)
31-50: LGTM: Clustering logic is well-structured.The zoom-based clustering approach is sensible:
- High zoom (≥14) shows individual trees
- Lower zoom levels group trees by geohash precision
- The
minClusterSizeparameter provides flexibilityThe implementation correctly handles edge cases like empty input and groups below the minimum cluster threshold.
|
|
||
| PINATA_API_KEY= | ||
| PINATA_API_SECRET= | ||
| WEB3_STORAGE_TOKEN= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check that .env is properly ignored
echo "=== Checking .gitignore for .env ==="
if grep -q "^\.env$" .gitignore; then
echo "✓ .env is in .gitignore"
else
echo "✗ WARNING: .env is not in .gitignore!"
fi
echo -e "\n=== Checking for any committed .env files ==="
git ls-files | grep "\.env$" || echo "✓ No .env files committed"
echo -e "\n=== Searching for hardcoded tokens in code ==="
rg -i "web3.*storage.*token" -C2 | grep -v "dotenv\|getenv\|env\[" || echo "✓ No hardcoded tokens found"Repository: StabilityNexus/Treee
Length of output: 391
🏁 Script executed:
# Check for .gitignore existence and content
cat .gitignore | head -30Repository: StabilityNexus/Treee
Length of output: 604
🏁 Script executed:
# Search for any token management or CI/CD configuration
fd -e "\.yml$" -e "\.yaml$" -o -e "\.json$" | grep -E "(ci|workflow|pipeline|github|gitlab|circleci)" | head -20Repository: StabilityNexus/Treee
Length of output: 180
🏁 Script executed:
# Look for documentation on token/secret management
fd -e "\.md$" | xargs rg -l "token\|secret\|credential\|environment" | head -10Repository: StabilityNexus/Treee
Length of output: 46
🏁 Script executed:
# Show full .gitignore
cat .gitignoreRepository: StabilityNexus/Treee
Length of output: 3644
🏁 Script executed:
# Search for CI/CD configuration files with corrected syntax
find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name "Dockerfile" \) | grep -v node_modules | head -20Repository: StabilityNexus/Treee
Length of output: 135
🏁 Script executed:
# Check the ipfs_services.dart file to see token handling
cat lib/utils/services/ipfs_services.dart | head -50Repository: StabilityNexus/Treee
Length of output: 994
🏁 Script executed:
# Search for documentation files mentioning environment, tokens, or deployment
find . -name "*.md" -type f | xargs grep -l -i "environment\|token\|deployment\|ci\|secret" 2>/dev/null | head -10Repository: StabilityNexus/Treee
Length of output: 135
🏁 Script executed:
# Check if .env.stencil exists and verify migration context
ls -la .env* 2>/dev/null && echo "---" && cat .env.stencilRepository: StabilityNexus/Treee
Length of output: 480
🏁 Script executed:
# Check CI/CD workflow for environment variable handling
cat .github/workflows/flutter.yamlRepository: StabilityNexus/Treee
Length of output: 818
🏁 Script executed:
# Check README for deployment and token management information
cat README.md | head -100Repository: StabilityNexus/Treee
Length of output: 4225
🏁 Script executed:
# Search for any deployment or operations documentation
find . -type f \( -name "*deploy*" -o -name "*ops*" -o -name "*operation*" -o -name "CONTRIBUTING*" \) 2>/dev/nullRepository: StabilityNexus/Treee
Length of output: 46
🏁 Script executed:
# Check if there's any documentation about Web3.Storage setup
grep -r "Web3.Storage\|WEB3_STORAGE_TOKEN" . --include="*.md" --include="*.txt" 2>/dev/null | head -20Repository: StabilityNexus/Treee
Length of output: 150
Fix CI/CD workflow to inject Web3.Storage token via GitHub Secrets instead of using empty template.
The .env.stencil template is properly ignored and not committed, and the code correctly loads the token via flutter_dotenv. However, the CI/CD workflow copies .env.stencil directly without injecting the actual token, meaning builds would run with an empty WEB3_STORAGE_TOKEN.
Address the remaining security gaps:
- Update
.github/workflows/flutter.yamlto injectWEB3_STORAGE_TOKENfrom GitHub Secrets before the build step - Document the Web3.Storage token setup and permissions required (upload-only scope)
- Add deployment documentation covering environment setup for staging and production environments, including token rotation procedures
🤖 Prompt for AI Agents
In .env.stencil around line 3, the template leaves WEB3_STORAGE_TOKEN empty and
the CI workflow currently copies this file without injecting the real secret;
update .github/workflows/flutter.yaml to create or overwrite the env file during
the workflow using the GitHub Secret (e.g. write WEB3_STORAGE_TOKEN=${{
secrets.WEB3_STORAGE_TOKEN }} into the .env used by the build), ensure the
secret is referenced only at runtime and not printed in logs, and add repository
documentation: a short section describing how to create a Web3.Storage token
with upload-only scope, how to add it to GitHub Secrets for staging/production,
and a deployment subsection describing environment setup and token rotation
procedures.
| @override | ||
| void dispose() { | ||
| _mapController.dispose(); | ||
| super.dispose(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -name "pubspec.yaml" -type f | head -5Repository: StabilityNexus/Treee
Length of output: 78
🏁 Script executed:
rg "flutter_map" -A 2 -B 2Repository: StabilityNexus/Treee
Length of output: 2734
🏁 Script executed:
rg "MapController" -A 3 -B 1Repository: StabilityNexus/Treee
Length of output: 2848
🏁 Script executed:
rg "dispose\(" lib/pages/nearby_trees_map_page.dart lib/widgets/map_widgets/ -B 2 -A 1Repository: StabilityNexus/Treee
Length of output: 1548
🌐 Web query:
flutter_map 8.1.1 MapController dispose lifecycle API
💡 Result:
Short summary (flutter_map v8.1.1)
-
The MapController is a normal object you create and pass into FlutterMap; the plugin attaches that controller to the map and keeps it attached until the FlutterMap widget is disposed. Use a new MapController if you need a fresh controller when rebuilding. [1][2]
-
flutter_map does not provide a separate public "dispose" method on MapController for users to call; controller lifecycle is tied to the FlutterMap widget lifecycle (so it will be effectively detached when the map is disposed). Avoid reusing one external MapController across map widgets that get destroyed/rebuilt — this has produced lifecycle bugs (attachment/desync assertions) and is why the docs recommend creating/initializing the controller adjacent to the FlutterMap or reinitializing when needed. [2][4]
-
Practical guidance:
- Create the MapController next to the FlutterMap (or recreate it when rebuilding).
- If your map is in lazy lists (PageView/ListView) use MapOptions.keepAlive to retain state instead of relying on reusing the controller.
- If you need to manage external references, clear/unset them when the FlutterMap is disposed to avoid reattachment bugs. See the linked GitHub issue for known problems and discussion. [2][3][4]
Sources
- flutter_map changelog (v8.1.1). [pub.dev]. [1]
- flutter_map controller docs / usage (controller lifecycle notes). [fleaflet.dev]. [2]
- GitHub issue: external MapController lifecycle not synced (reports reattachment/desync bug). [github.com]. [3]
Remove _mapController.dispose() — MapController does not expose dispose() in flutter_map 8.1.1
flutter_map does not provide a public dispose() method on MapController. The controller's lifecycle is managed by the FlutterMap widget itself; calling dispose() on it will fail at compile or runtime. Remove the _mapController.dispose() call and rely on the widget lifecycle cleanup instead.
Also note: this same incorrect pattern is present in lib/widgets/map_widgets/flutter_map_widget.dart (3 instances) and lib/widgets/map_widgets/static_map_display_widget.dart.
🤖 Prompt for AI Agents
In lib/pages/nearby_trees_map_page.dart around lines 42–46, remove the call to
_mapController.dispose() because flutter_map 8.1.1's MapController has no public
dispose() and its lifecycle is managed by FlutterMap; simply call
super.dispose() only. Also remove the same erroneous dispose() calls in
lib/widgets/map_widgets/flutter_map_widget.dart (three instances) and
lib/widgets/map_widgets/static_map_display_widget.dart so none of the files call
dispose() on a MapController; rely on the widget tree to clean up instead.
| 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; | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard all post-await setState calls with mounted (avoid “setState() called after dispose”)
You check mounted once (Line 69-70), but there’s another await (Line 76-81) after it, and both the success and error paths call setState. Add a mounted check immediately before each setState that happens after an await.
@@
Future<void> _loadNearbyTrees() async {
setState(() {
_isLoading = true;
_hasError = false;
_errorMessage = null;
});
@@
- final result = await ContractReadFunctions.getNearbyTrees(
+ 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 trees = result.data['trees'] as List<Map<String, dynamic>>;
-
- setState(() {
+ final trees = (result.data['trees'] as List).cast<Map<String, dynamic>>();
+
+ setState(() {
_nearbyTrees = trees;
_isLoading = false;
});
@@
} catch (e) {
logger.e("Error loading nearby trees: $e");
-
- // Use fallback location if not already set
- setState(() {
+
+ if (!mounted) return;
+
+ // Use fallback location if not already set
+ 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.
| 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 trees = (result.data['trees'] as List).cast<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"); | |
| if (!mounted) return; | |
| // Use fallback location if not already set | |
| setState(() { | |
| _userLat ??= _defaultLat; | |
| _userLng ??= _defaultLng; | |
| _hasError = true; | |
| _errorMessage = e.toString(); | |
| _isLoading = false; | |
| }); | |
| } | |
| } |
🤖 Prompt for AI Agents
In lib/pages/nearby_trees_map_page.dart around lines 48-107, add mounted guards
before every setState that runs after an await: before calling setState
following the ContractReadFunctions.getNearbyTrees await (the success path where
you assign _nearbyTrees and _isLoading) check if (!mounted) return; and
similarly before the setState in the catch block that sets
_userLat/_userLng/_hasError/_errorMessage/_isLoading, check if (!mounted)
return; (the initial setState at the top is before awaits and can remain
unchanged).
| /// Convert contract coordinates (fixed-point) to decimal degrees | ||
| double _convertCoordinate(int coordinate) { | ||
| return (coordinate / 1000000.0) - 90.0; | ||
| } | ||
|
|
||
| List<Marker> _buildTreeMarkers() { | ||
| return _nearbyTrees.map((tree) { | ||
| final lat = _convertCoordinate(tree['latitude'] as int); | ||
| final lng = _convertCoordinate(tree['longitude'] as int); | ||
| final isSelected = _selectedTreeId == tree['id']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Search for coordinate-related code and contract service references
rg -i "coordinate|latitude|longitude" --type dart -A 3 -B 1 | head -100Repository: StabilityNexus/Treee
Length of output: 7452
🏁 Script executed:
# Look for contract service or helper files
fd -e dart | xargs grep -l "contract\|web3\|ethers" | head -20Repository: StabilityNexus/Treee
Length of output: 1422
🏁 Script executed:
# Search for constants or configuration related to coordinate encoding
rg -i "offset|scaling|1000000|90\.0|180\.0" --type dart -B 2 -A 2Repository: StabilityNexus/Treee
Length of output: 50376
🏁 Script executed:
# Look for any Tree NFT or contract ABI files
fd "contract\|abi\|tree.*nft" -iRepository: StabilityNexus/Treee
Length of output: 46
🏁 Script executed:
# Check if there's a service class handling coordinate conversion
rg "ConvertCoordinate|CoordinateConverter|_convert" --type dart -B 2 -A 5Repository: StabilityNexus/Treee
Length of output: 10513
🏁 Script executed:
# Get the exact coordinate encoding from the contract write functions
cat -n lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_write_functions.dart | grep -A 5 -B 5 "latitude + 90\|longitude + 180"Repository: StabilityNexus/Treee
Length of output: 644
🏁 Script executed:
# Check all files that use _convertCoordinate to see the scope of the issue
rg "_convertCoordinate" --type dart -B 2 -A 8 | head -150Repository: StabilityNexus/Treee
Length of output: 12126
Critical: Longitude coordinate conversion uses wrong offset
The contract encodes coordinates as lat = (latitude + 90) * 1e6 and lng = (longitude + 180) * 1e6. Your decode applies -90.0 to both, which is incorrect for longitude. This causes all longitudes to be offset by 90 degrees. Split the conversion methods to apply the correct offsets:
- double _convertCoordinate(int coordinate) {
- return (coordinate / 1000000.0) - 90.0;
- }
+ double _convertLatitude(int coordinate) => (coordinate / 1000000.0) - 90.0;
+ double _convertLongitude(int coordinate) => (coordinate / 1000000.0) - 180.0;Then update all call sites in this file to use the appropriate method for each coordinate type.
Note: This bug also exists in lib/services/tree_clustering_service.dart, lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart, lib/widgets/nft_display_utils/recent_trees_widget.dart, and lib/pages/tree_details_page.dart with the same incorrect -90.0 offset applied to longitudes. Consider extracting a shared helper method to prevent future inconsistencies.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In lib/pages/nearby_trees_map_page.dart around lines 115 to 124, the coordinate
decoding uses the same -90.0 offset for both latitude and longitude; change this
by splitting the conversion into two methods (e.g., _convertLatitude and
_convertLongitude) where latitude = (coordinate / 1e6) - 90.0 and longitude =
(coordinate / 1e6) - 180.0, then update the call sites in this file to call the
appropriate method for tree['latitude'] and tree['longitude']; also consider
extracting these helpers into a shared utility and replicate the fix in the
other listed files to avoid future inconsistencies.
| List<Marker> _buildTreeMarkers() { | ||
| return _nearbyTrees.map((tree) { | ||
| final lat = _convertCoordinate(tree['latitude'] as int); | ||
| final lng = _convertCoordinate(tree['longitude'] as int); | ||
| final isSelected = _selectedTreeId == tree['id']; | ||
|
|
||
| return Marker( | ||
| point: LatLng(lat, lng), | ||
| width: 40, | ||
| height: 50, | ||
| child: GestureDetector( | ||
| onTap: () { | ||
| setState(() { | ||
| _selectedTreeId = tree['id']; | ||
| }); | ||
| _showTreeDetails(tree); | ||
| }, | ||
| child: Icon( | ||
| Icons.park, | ||
| size: isSelected ? 45 : 35, | ||
| color: isSelected ? Colors.green[700] : Colors.green, | ||
| shadows: [ | ||
| Shadow( | ||
| color: Colors.black.withValues(alpha: 0.3), | ||
| blurRadius: 4, | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ); | ||
| }).toList(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marker selection/type robustness: normalize tree['id'] (int vs BigInt/String)
_selectedTreeId == tree['id'] (Line 124) and assignment (Line 133) assume tree['id'] is an int. If the contract layer returns BigInt (common in web3), selection highlighting and navigation can break at runtime. Consider normalizing IDs (final treeId = (tree['id'] as num).toInt() or explicit BigInt handling) at ingestion time (in _loadNearbyTrees).
🤖 Prompt for AI Agents
In lib/pages/nearby_trees_map_page.dart around lines 120 to 151, the code
compares and assigns tree IDs assuming tree['id'] is an int which breaks when
the ID is a BigInt or String; normalize the ID to an int before use (either
during ingestion in _loadNearbyTrees or at the top of _buildTreeMarkers) by
detecting BigInt and calling toInt(), parsing numeric Strings, or casting nums
toInt(), then use that normalized treeId for _selectedTreeId comparisons and
assignments and when passing to any navigation/detail functions.
| static String encode(double lat, double lng, {int precision = 7}) { | ||
| final latRange = [-90.0, 90.0]; | ||
| final lngRange = [-180.0, 180.0]; | ||
| final geohash = StringBuffer(); | ||
| var isEven = true; | ||
| var bit = 0; | ||
| var ch = 0; | ||
|
|
||
| while (geohash.length < precision) { | ||
| if (isEven) { | ||
| // Longitude | ||
| final mid = (lngRange[0] + lngRange[1]) / 2; | ||
| if (lng > mid) { | ||
| ch |= (1 << (4 - bit)); | ||
| lngRange[0] = mid; | ||
| } else { | ||
| lngRange[1] = mid; | ||
| } | ||
| } else { | ||
| // Latitude | ||
| final mid = (latRange[0] + latRange[1]) / 2; | ||
| if (lat > mid) { | ||
| ch |= (1 << (4 - bit)); | ||
| latRange[0] = mid; | ||
| } else { | ||
| latRange[1] = mid; | ||
| } | ||
| } | ||
|
|
||
| isEven = !isEven; | ||
|
|
||
| if (bit < 4) { | ||
| bit++; | ||
| } else { | ||
| geohash.write(_base32[ch]); | ||
| bit = 0; | ||
| ch = 0; | ||
| } | ||
| } | ||
|
|
||
| return geohash.toString(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Add input validation to prevent invalid coordinate encoding.
The encode method doesn't validate that latitude is within [-90, 90] and longitude within [-180, 180]. Invalid coordinates could produce incorrect geohashes or cause unexpected behavior in the binary search logic.
Add validation at the start of the method:
static String encode(double lat, double lng, {int precision = 7}) {
+ if (lat < -90 || lat > 90) {
+ throw ArgumentError('Latitude must be between -90 and 90, got $lat');
+ }
+ if (lng < -180 || lng > 180) {
+ throw ArgumentError('Longitude must be between -180 and 180, got $lng');
+ }
+ if (precision < 1 || precision > 12) {
+ throw ArgumentError('Precision must be between 1 and 12, got $precision');
+ }
+
final latRange = [-90.0, 90.0];🤖 Prompt for AI Agents
In lib/utils/geohash_utils.dart around lines 22 to 63, the encode method lacks
validation for latitude/longitude input; add checks at the start to ensure lat
is within -90..90 and lng within -180..180 and throw a descriptive
RangeError/ArgumentError if out of range so invalid coordinates are rejected
immediately. Also validate precision is a positive integer (and optionally
enforce a sensible max like 12) and throw if invalid. Keep the rest of the
encoding logic unchanged.
| static LatLng decode(String geohash) { | ||
| final latRange = [-90.0, 90.0]; | ||
| final lngRange = [-180.0, 180.0]; | ||
| var isEven = true; | ||
|
|
||
| for (var i = 0; i < geohash.length; i++) { | ||
| final char = geohash[i]; | ||
| final cd = _base32.indexOf(char); | ||
|
|
||
| for (var j = 0; j < 5; j++) { | ||
| final mask = 1 << (4 - j); | ||
|
|
||
| if (isEven) { | ||
| // Longitude | ||
| if (cd & mask != 0) { | ||
| lngRange[0] = (lngRange[0] + lngRange[1]) / 2; | ||
| } else { | ||
| lngRange[1] = (lngRange[0] + lngRange[1]) / 2; | ||
| } | ||
| } else { | ||
| // Latitude | ||
| if (cd & mask != 0) { | ||
| latRange[0] = (latRange[0] + latRange[1]) / 2; | ||
| } else { | ||
| latRange[1] = (latRange[0] + latRange[1]) / 2; | ||
| } | ||
| } | ||
|
|
||
| isEven = !isEven; | ||
| } | ||
| } | ||
|
|
||
| final lat = (latRange[0] + latRange[1]) / 2; | ||
| final lng = (lngRange[0] + lngRange[1]) / 2; | ||
|
|
||
| return LatLng(lat, lng); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Add validation for geohash format in decode.
The decode method doesn't validate the input geohash string. Invalid characters or empty strings could cause index errors when looking up characters in _base32.
Add validation at the start of the method:
static LatLng decode(String geohash) {
+ if (geohash.isEmpty) {
+ throw ArgumentError('Geohash cannot be empty');
+ }
+
+ // Validate all characters are valid base32
+ for (var char in geohash.split('')) {
+ if (!_base32.contains(char)) {
+ throw ArgumentError('Invalid geohash character: $char');
+ }
+ }
+
final latRange = [-90.0, 90.0];📝 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.
| static LatLng decode(String geohash) { | |
| final latRange = [-90.0, 90.0]; | |
| final lngRange = [-180.0, 180.0]; | |
| var isEven = true; | |
| for (var i = 0; i < geohash.length; i++) { | |
| final char = geohash[i]; | |
| final cd = _base32.indexOf(char); | |
| for (var j = 0; j < 5; j++) { | |
| final mask = 1 << (4 - j); | |
| if (isEven) { | |
| // Longitude | |
| if (cd & mask != 0) { | |
| lngRange[0] = (lngRange[0] + lngRange[1]) / 2; | |
| } else { | |
| lngRange[1] = (lngRange[0] + lngRange[1]) / 2; | |
| } | |
| } else { | |
| // Latitude | |
| if (cd & mask != 0) { | |
| latRange[0] = (latRange[0] + latRange[1]) / 2; | |
| } else { | |
| latRange[1] = (latRange[0] + latRange[1]) / 2; | |
| } | |
| } | |
| isEven = !isEven; | |
| } | |
| } | |
| final lat = (latRange[0] + latRange[1]) / 2; | |
| final lng = (lngRange[0] + lngRange[1]) / 2; | |
| return LatLng(lat, lng); | |
| } | |
| static LatLng decode(String geohash) { | |
| if (geohash.isEmpty) { | |
| throw ArgumentError('Geohash cannot be empty'); | |
| } | |
| // Validate all characters are valid base32 | |
| for (var char in geohash.split('')) { | |
| if (!_base32.contains(char)) { | |
| throw ArgumentError('Invalid geohash character: $char'); | |
| } | |
| } | |
| final latRange = [-90.0, 90.0]; | |
| final lngRange = [-180.0, 180.0]; | |
| var isEven = true; | |
| for (var i = 0; i < geohash.length; i++) { | |
| final char = geohash[i]; | |
| final cd = _base32.indexOf(char); | |
| for (var j = 0; j < 5; j++) { | |
| final mask = 1 << (4 - j); | |
| if (isEven) { | |
| // Longitude | |
| if (cd & mask != 0) { | |
| lngRange[0] = (lngRange[0] + lngRange[1]) / 2; | |
| } else { | |
| lngRange[1] = (lngRange[0] + lngRange[1]) / 2; | |
| } | |
| } else { | |
| // Latitude | |
| if (cd & mask != 0) { | |
| latRange[0] = (latRange[0] + latRange[1]) / 2; | |
| } else { | |
| latRange[1] = (latRange[0] + latRange[1]) / 2; | |
| } | |
| } | |
| isEven = !isEven; | |
| } | |
| } | |
| final lat = (latRange[0] + latRange[1]) / 2; | |
| final lng = (lngRange[0] + lngRange[1]) / 2; | |
| return LatLng(lat, lng); | |
| } |
🤖 Prompt for AI Agents
In lib/utils/geohash_utils.dart around lines 68 to 104, the decode method lacks
input validation so empty strings or characters not in the geohash alphabet can
cause index errors; at the start of the method validate that the input is
non-null/non-empty and that every character exists in the allowed _base32 string
(optionally normalize case to the expected case before checking), and throw an
ArgumentError (or similar) with a clear message if validation fails so the
function exits early on invalid geohash values.
| /// Convert contract fixed-point coordinate to decimal degrees | ||
| static double _convertCoordinate(int coordinate) { | ||
| return (coordinate / 1000000.0) - 90.0; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for coordinate storage/conversion logic and the 1000000 factor
rg -n "1000000|coordinate" --type dart lib/utils/services/contract_functions/tree_nft_contract/ -B 2 -A 2Repository: StabilityNexus/Treee
Length of output: 3445
🏁 Script executed:
#!/bin/bash
# Look for usages of _convertCoordinate function
rg -n "_convertCoordinate" --type dart -B 3 -A 3Repository: StabilityNexus/Treee
Length of output: 10174
🏁 Script executed:
#!/bin/bash
# Search for latitude and longitude handling
rg -n "latitude|longitude" --type dart lib/utils/services/contract_functions/ -B 2 -A 2 | head -80Repository: StabilityNexus/Treee
Length of output: 8535
Longitude conversion uses wrong offset and produces incorrect results.
The function incorrectly uses - 90.0 for both latitude and longitude. Based on the contract's encoding logic (visible in tree_nft_contract_write_functions.dart lines 65-66):
- Latitude is encoded as
(latitude + 90.0) * 1e6→ decode requires- 90.0✓ - Longitude is encoded as
(longitude + 180.0) * 1e6→ decode requires- 180.0✗
The current formula (coordinate / 1e6) - 90.0 produces longitude in the range [-90, 270] instead of the correct [-180, 180]. For example, longitude 0° (encoded as 180,000,000) decodes to 90° instead of 0°.
This affects multiple files using the same flawed conversion: nearby_trees_map_page.dart, tree_clustering_service.dart, recent_trees_widget.dart, and the reviewed file. Change the offset to - 180.0 for longitude values, or create separate conversion functions for latitude vs. longitude.
🤖 Prompt for AI Agents
In
lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart
around lines 506 to 509, the coordinate decoder uses a fixed -90.0 offset which
is only correct for latitude; longitude was encoded as (longitude + 180.0) * 1e6
and must be decoded with -180.0. Fix by splitting into two functions (or adding
a boolean/enum) so latitude uses (coordinate / 1e6) - 90.0 and longitude uses
(coordinate / 1e6) - 180.0, update all call sites (nearby_trees_map_page.dart,
tree_clustering_service.dart, recent_trees_widget.dart and this file) to call
the appropriate decoder, and add unit tests verifying decode(180000000) => 0°
longitude and decode(90000000) => 0° latitude.
|
|
||
| String apiKey = dotenv.get('PINATA_API_KEY', fallback: ""); | ||
| String apiSecret = dotenv.get('PINATA_API_SECRET', fallback: ""); | ||
| String apiToken = dotenv.get('WEB3_STORAGE_TOKEN', fallback: ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing validation for empty API token.
The apiToken falls back to an empty string if WEB3_STORAGE_TOKEN is not set, which will cause authentication failures without a clear error message. Consider validating this at upload time.
String apiToken = dotenv.get('WEB3_STORAGE_TOKEN', fallback: "");Add validation in uploadToIPFS:
Future<String?> uploadToIPFS(
File imageFile, Function(bool) setUploadingState) async {
if (apiToken.isEmpty) {
// Consider logging or throwing an error
return null;
}
setUploadingState(true);
// ...🤖 Prompt for AI Agents
In lib/utils/services/ipfs_services.dart around line 6, the apiToken is set with
a fallback to an empty string which can cause silent auth failures; add a
validation at the start of uploadToIPFS to check if apiToken.isEmpty and if so
immediately return null (or throw) and optionally log a clear error before any
network calls or toggling uploading state, ensure setUploadingState(true) is
only called after the token is validated, and make sure any early-return resets
uploading state or leaves it unchanged as appropriate.
| Expanded( | ||
| child: Text( | ||
| 'Location: ${(tree.latitude / 1000000) - 90}, ${(tree.longitude / 1000000) - 180}', | ||
| 'Location: ${(tree.latitude / 1000000) - 90}, ${(tree.longitude / 1000000) - 90}', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify coordinate conversion consistency across the codebase
echo "=== Searching for coordinate conversion patterns ==="
rg -n "/ 1[0e]6.*- (90|180)" --type dart -C2
echo -e "\n=== Searching for latitude/longitude field usage ==="
rg -n "\.latitude|\.longitude" --type dart -A1 -B1 | head -100Repository: StabilityNexus/Treee
Length of output: 6587
🏁 Script executed:
#!/bin/bash
# Search for recent_trees_widget.dart
echo "=== Searching for recent_trees_widget.dart ==="
fd -n "recent_trees_widget" --type f
echo -e "\n=== Search for coordinate-related constants or documentation ==="
rg -n "coordinate|contract.*format|1e6|1000000" --type dart -i | grep -E "(const|final|//|comment)" | head -30
echo -e "\n=== Check for smart contract ABI or constants files ==="
fd -n "(abi|contract|constant)" --type dart --type json | head -20Repository: StabilityNexus/Treee
Length of output: 4297
🏁 Script executed:
#!/bin/bash
# Look at the _convertCoordinate function implementation
echo "=== _convertCoordinate function in tree_clustering_service.dart ==="
rg -A5 "_convertCoordinate" lib/services/tree_clustering_service.dart
echo -e "\n=== _convertCoordinate function in nearby_trees_map_page.dart ==="
rg -A5 "_convertCoordinate" lib/pages/nearby_trees_map_page.dart
echo -e "\n=== _convertCoordinate function in tree_nft_contract_read_services.dart ==="
rg -A5 "_convertCoordinate" lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dartRepository: StabilityNexus/Treee
Length of output: 2789
🏁 Script executed:
#!/bin/bash
# Verify the coordinate encoding in write functions
echo "=== Coordinate encoding in tree_nft_contract_write_functions.dart ==="
sed -n '60,70p' lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_write_functions.dart
echo -e "\n=== Coordinate encoding in organisation_write_functions.dart ==="
sed -n '125,135p' lib/utils/services/contract_functions/organisation_contract/organisation_write_functions.dart
echo -e "\n=== Verify all usages of _convertCoordinate ==="
rg "_convertCoordinate" --type dart -cRepository: StabilityNexus/Treee
Length of output: 1285
Fix longitude coordinate conversion offset across the codebase.
The longitude conversion uses an offset of 90 instead of the correct 180. Smart contract encoding stores coordinates as (latitude + 90.0) * 1e6 and (longitude + 180.0) * 1e6, so decoding must use - 90 for latitude and - 180 for longitude. Currently, all usages apply - 90 to both, causing longitude values to be incorrect by approximately 90 degrees.
This bug affects:
user_nfts_widget.dartline 255:(tree.longitude / 1000000) - 90tree_details_page.dartlines 194, 246tree_clustering_service.dart,nearby_trees_map_page.dart, andtree_nft_contract_read_services.dart— all using_convertCoordinate()for both latitude and longitude
Update the conversion logic to apply the correct offset for each coordinate type.
🤖 Prompt for AI Agents
In lib/widgets/nft_display_utils/user_nfts_widget.dart around line 255, the
longitude is decoded with the wrong offset: change the decoding so latitude uses
(value / 1e6) - 90 and longitude uses (value / 1e6) - 180; update the literal at
line 255 to use the correct -180 offset for longitude and audit/adjust any
shared helper (_convertCoordinate) to accept a coordinate type or separate
latitude/longitude converters so longitude applies -180 while latitude remains
-90, then propagate the fix to the other files noted (tree_details_page.dart,
tree_clustering_service.dart, nearby_trees_map_page.dart,
tree_nft_contract_read_services.dart) so all longitude decodings use -180.
Pinata IPFS : ~1 GB free storage , however Web3.storage : ~5 GB free storage . making it better alternative.
Closes #25
Work along #31
This pull request introduces a new "Nearby Trees Map" feature, allowing users to view and interact with trees located near a specific geographic area. The update includes backend logic for geospatial clustering and geohash-based proximity search, UI enhancements for map access, and minor bug fixes. Below are the most important changes grouped by theme:
Nearby Trees Map Feature & UI Integration
NearbyTreesMapPage), including navigation from the all-trees page and a new "Map" button for quick access. [1] [2] [3] [4]README.mdto reflect the use of Web3.Storage/Storacha for decentralized storage.Geospatial Search and Clustering Logic
GeohashUtilsinlib/utils/geohash_utils.dartfor geohash encoding/decoding, neighbor lookups, coverage calculation, and distance computation, enabling efficient spatial queries.TreeClusteringServiceinlib/services/tree_clustering_service.dartto group trees into clusters based on map zoom level and proximity, improving map visualization performance and clarity.getNearbyTreesthat uses geohash-based filtering and distance checks to efficiently find trees near a given location. [1] [2]Bug Fixes & UI Improvements
TreeDetailsPageto ensure correct longitude calculation and display. [1] [2]Summary by CodeRabbit
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.