-
Notifications
You must be signed in to change notification settings - Fork 19
Solving #24 Dynamic map that renders nearby planted trees #31
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
WalkthroughThis 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45–75 minutes Areas requiring extra attention:
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 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: 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 likeRouteConstants.nearbyTreesPath(and maybe aRouteConstants.nearbyTreesname) 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.0is correct, consider replacing these inline conversions with a shared helper to prevent future mismatches.lib/pages/trees_page.dart (1)
56-85: Usego_routernavigation here (avoid mixingNavigator.pushandcontext.push).This app already has a
/nearby-treesroute; usingNavigator.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
_convertCoordinatemethod 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 inlib/utils/to reduce maintenance burden and ensure consistency.Note: The
as intcasts are safe in current implementation since the contract read services already convertBigInttointbefore 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.dartis 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.
coverageGeohashesis calculated and logged but not used to filter trees. All trees fromgetRecentTreesPaginatedare iterated and filtered by distance only. While the TODO comment acknowledges this, the geohash calculation is currently wasted computation.Consider either:
- Removing the geohash calculation until the contract supports geohash queries
- Using geohashes for client-side pre-filtering before distance calculation
lib/services/tree_clustering_service.dart (2)
139-142: Duplicated coordinate conversion logic.
_convertCoordinateis duplicated here and intree_nft_contract_read_services.dart. Consider extracting this to a shared utility (e.g., inGeohashUtilsor 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 becauseclusterTreesreturns early at line 39 whenzoomLevel >= 14. The function_getClusterDistanceis only called whenzoomLevel < 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 > 630returns 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
📒 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: ConfirmMapController.dispose()is valid forflutter_map8.1.1.If
MapControllerdoesn’t exposedispose()in your pinned version, this won’t compile (or you may be double-disposing via inherited behavior). Please verify against theflutter_map8.1.1 API.lib/services/tree_clustering_service.dart (1)
31-50: LGTM on high-zoom bypass logic.The early return for
zoomLevel >= 14correctly 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.
getNeighborscalls_getNeighborrecursively for diagonal neighbors (e.g., line 117:_getNeighbor(_getNeighbor(geohash, 'top'), 'right')). If the inner_getNeighborreturns an empty string (which can happen at extreme boundaries whenparent.isEmptyat 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.
| // Default location (fallback if GPS fails) | ||
| static const double _defaultLat = 28.9845; // Example: Roorkee, India | ||
| static const double _defaultLng = 77.8956; | ||
|
|
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.
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.
| 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 setState after async awaits + avoid unsafe casts from result.data.
- You check
mountedbefore the contract call, but the widget can still unmount while awaitinggetNearbyTrees(). result.data['trees'] as List<Map<String, dynamic>>is a common runtime crash if the value isList<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.
| 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; | |
| }); | |
| } | |
| } |
| 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), | ||
| ), | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ); | ||
| } |
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.
Bottom sheet: null-safe photo count + consider centering map on selected marker.
tree['photos']might be null/missing;(tree['photos'] as List).lengthcan 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.
| 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(), | ||
| ], | ||
| ), | ||
| ], | ||
| ), |
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:
# 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.dartRepository: StabilityNexus/Treee
Length of output: 104
🏁 Script executed:
# Search for any attribution-related code in the entire codebase
rg -i "attribution|openstreetmap|© |license" --type dartRepository: 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 dartRepository: 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 dartRepository: 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.
| // 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'), | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ), | ||
|
|
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.
Loading/error overlays should respect theme and correct messaging.
- Hardcoding
Colors.whitewill 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.
| hintStyle: TextStyle( | ||
| color: getThemeColors(context)['textSecondary'], | ||
| color: Colors.grey.shade600, | ||
| ), | ||
| border: OutlineInputBorder( |
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.
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.
| // 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; | ||
| } |
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.
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 countCommittable 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.
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:
NearbyTreesMapPageand integrated it into the app's navigation using GoRouter and manual navigation from relevant pages (main.dart,trees_page.dart, andrecent_trees_widget.dart). This allows users to view trees near their location on a map. [1] [2] [3] [4] [5]Geospatial Clustering and Utilities:
tree_clustering_service.dartto efficiently group trees for map display based on proximity and zoom level, using geohash-based clustering.geohash_utils.dartproviding geohash encoding/decoding, neighbor search, optimal precision calculation, and Haversine distance functions for spatial queries.Blockchain Spatial Search:
getNearbyTreesintree_nft_contract_read_services.dartto fetch and filter trees within a radius of a given location, using geohash coverage and distance calculations. [1] [2]Coordinate Conversion Bug Fixes:
UI/UX Improvements:
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
NearbyTreesMapPageand navigation routes/buttons to access the map from multiple locations in the app. [1] [2] [3] [4] [5]Geospatial Clustering and Utilities
TreeClusteringServicefor clustering trees by proximity and zoom, andGeohashUtilsfor geohash encoding, decoding, neighbor search, and distance calculation. [1] [2]Blockchain Spatial Search
getNearbyTreesto efficiently fetch and filter trees within a radius using geohash coverage and Haversine distance. [1] [2]Coordinate Conversion Bug Fixes
-180.0to-90.0in tree details and user NFT widgets for accurate location display. [1] [2] [3]UI/UX Improvements
Fixes Dynamic map that renders nearby planted trees #24
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.