Skip to content

Conversation

@LogicalGuy77
Copy link

@LogicalGuy77 LogicalGuy77 commented Dec 13, 2025

Fixes: #24

Summary by CodeRabbit

  • New Features

    • Interactive Map view added to bottom navigation with its own route.
    • Shows tree markers, user location, and map controls (zoom, center, load) with loading state.
    • Tree info card with details and “View Full Details” navigation.
    • Wallet-gated tree loading with clear empty/error feedback.
  • Improvements

    • Server-side and local tree lookup tuned for more relevant nearby results.
    • More accurate latitude/longitude formatting across listings.

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 13, 2025

Walkthrough

Adds a new map feature: route and bottom-nav entry, MapViewPage UI with wallet/user-location gating, MapProvider state, map UI widgets, and TreeMapService to fetch/filter tree NFTs by bounding box or geohash.

Changes

Cohort / File(s) Summary
App bootstrap & Routing
lib/main.dart, lib/utils/constants/route_constants.dart, lib/utils/constants/bottom_nav_constants.dart
Registered MapProvider in MultiProvider; added GoRoute for RouteConstants.mapViewPath -> MapViewPage; added RouteConstants.mapView/mapViewPath; added "Map" bottom-nav item linking to that route.
Map Page
lib/pages/map_view_page.dart
Added MapViewPage (stateful) that requests user location, gates tree loading on wallet connection, fetches trees using bounding box via TreeMapService, updates MapProvider, handles map move/zoom events, renders markers/user location, map controls, TreeInfoCard, and navigates to tree details.
State Management
lib/providers/map_provider.dart
Added MapProvider (extends ChangeNotifier) tracking center/zoom, optional userLocation, loadedTrees, loading/error flags; exposes getters/setters, refetch heuristics, bounding-box calculation, tree add/remove/update/clear, centerOnUser and reset.
Geospatial & Data Service
lib/utils/services/tree_map_service.dart
Added TreeMapService with methods to fetch trees by bounding box or geohash, parse/convert contract data, geohash encode/decode and neighbors, distance calculation, and safe error handling.
Map Widgets
lib/widgets/map_widgets/tree_map_widgets.dart
Added TreeMarkerWidget, UserLocationMarker, MapControlsWidget, and TreeInfoCard for marker/user rendering, controls (zoom/center/load), loading states, and tree detail display.
Coordinate helpers updates
lib/widgets/nft_display_utils/recent_trees_widget.dart, lib/widgets/organisation_details_page/tabs/planting_proposals_tab.dart
Replaced single coordinate converter with _convertLatitude and _convertLongitude; adjusted conversion formulas and updated call sites.

Sequence Diagram

sequenceDiagram
    actor User
    participant MapViewPage
    participant LocationService
    participant WalletProvider
    participant MapProvider
    participant TreeMapService
    participant ContractReadFunctions

    User->>MapViewPage: Open /map
    MapViewPage->>LocationService: request user location
    LocationService-->>MapViewPage: return coords
    MapViewPage->>MapProvider: setUserLocation(coords)

    MapViewPage->>WalletProvider: isConnected?
    alt wallet connected
        MapViewPage->>MapProvider: getBoundingBox()
        MapViewPage->>TreeMapService: getTreesInBoundingBox(bounds, max=100)
        TreeMapService->>ContractReadFunctions: fetch recent tree entries
        ContractReadFunctions-->>TreeMapService: raw entries
        TreeMapService-->>MapViewPage: parsed & filtered trees
        MapViewPage->>MapProvider: setLoadedTrees(trees)
        MapViewPage->>User: render markers & controls
    else wallet not connected
        MapViewPage-->>User: prompt to connect wallet
    end

    User->>MapViewPage: tap marker
    MapViewPage->>MapViewPage: select tree, show TreeInfoCard
    User->>MapViewPage: "View Details"
    MapViewPage->>Router: navigate to /trees/{id}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Review focus:
    • TreeMapService: geohash logic, coordinate conversion, parsing of contract data, and distance math.
    • MapViewPage: lifecycle/async flows (location, wallet gating), map event handlers, and navigation.
    • MapProvider: refetch heuristics, bounding-box correctness, concurrency and notifyListeners usage.
    • Widget/state interactions: marker taps -> provider updates -> UI overlays.

Possibly related PRs

  • Merged org trees #21 — overlaps on recent trees UI and coordinate-conversion logic (shared files: recent_trees_widget.dart, planting_proposals_tab.dart).
  • NFT Display page #13 — related changes around tree NFT data handling and mapping components/services.
  • Organisations page #16 — related route additions and GoRouter configuration edits.

Suggested reviewers

  • ceilican
  • Zahnentferner

Poem

🐰 I hopped where geohashes glow,

tiny markers line the map below.
Wallet first, then trees appear,
tap a card — the details near.
Hooray — a leafy path to show! 🌳🗺️

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature addition: a map view with tree markers and user location, which is the primary focus of the PR.
Linked Issues check ✅ Passed All coding requirements from issue #24 are met: dynamic map implemented, trees fetched via blockchain using geohash-based spatial indexing, flutter_map library used, user location integration, and trees displayed around user location.
Out of Scope Changes check ✅ Passed Minor refactoring to coordinate conversion methods in two existing widgets is aligned with the new coordinate handling introduced by the map feature and is not out of scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 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 (7)
lib/utils/services/tree_map_service.dart (1)

26-30: Consider making the fetch limit configurable or documenting the limitation.

Both getTreesInBoundingBox and getTreesNearLocation fetch only 50 trees from the contract, then filter client-side. For large map areas, this may miss many trees. Consider either:

  1. Making the limit configurable via a parameter
  2. Implementing pagination to fetch all matching trees
  3. Documenting this limitation clearly
  static Future<List<Tree>> getTreesInBoundingBox({
    required WalletProvider walletProvider,
    required double minLat,
    required double maxLat,
    required double minLng,
    required double maxLng,
    int maxTrees = 100,
+   int fetchLimit = 50, // Backend fetch limit before filtering
  }) async {

Also applies to: 92-96

lib/pages/map_view_page.dart (3)

147-149: Eliminate code duplication by reusing TreeMapService._convertCoordinate.

This method duplicates the logic from TreeMapService._convertCoordinate. Since the service method is private, consider either:

  1. Making TreeMapService._convertCoordinate public
  2. Creating a shared utility function

In tree_map_service.dart:

-  static double _convertCoordinate(int coordinate) {
+  static double convertCoordinate(int coordinate) {

Then in this file:

-  double _convertCoordinate(int coordinate) {
-    return (coordinate / 1000000.0) - 90.0;
-  }

And update usages:

-                final lat = _convertCoordinate(tree.latitude);
-                final lng = _convertCoordinate(tree.longitude);
+                final lat = TreeMapService.convertCoordinate(tree.latitude);
+                final lng = TreeMapService.convertCoordinate(tree.longitude);

87-89: Consider using a different UI pattern for empty results.

Setting an error message when no trees are found treats a valid empty state as an error. Consider showing a separate informational message or a different visual indicator instead of using the error banner.

      if (trees.isEmpty) {
-       mapProvider.setError("No trees found in this area. Try moving the map or zooming out.");
+       // Consider adding a separate info message state in MapProvider
+       // mapProvider.setInfoMessage("No trees found in this area. Try moving the map or zooming out.");
      }

291-291: withOpacity is deprecated in newer Flutter versions.

Consider using Color.fromARGB or withValues for better performance and forward compatibility.

-                   color: Colors.black.withOpacity(0.2),
+                   color: Colors.black.withValues(alpha: 0.2),

Also applies to: 328-328

lib/providers/map_provider.dart (2)

99-112: getBoundingBox() should be derived from actual visible bounds, not zoom-only heuristics.

0.05 / zoom is unlikely to match the map widget viewport and will vary across devices/aspect ratios. Prefer feeding the provider the real bounds from the map (MapCamera.visibleBounds / controller bounds) and returning that.

Minimal API change idea:

-  Map<String, double> getBoundingBox() {
-    final latDelta = 0.05 / _currentZoom;
-    final lngDelta = 0.05 / _currentZoom;
-    return { ... };
-  }
+  Map<String, double> getBoundingBoxFromBounds(LatLngBounds bounds) {
+    return {
+      'minLat': bounds.south,
+      'maxLat': bounds.north,
+      'minLng': bounds.west,
+      'maxLng': bounds.east,
+    };
+  }

(Then have the page pass bounds into the fetch call.)


35-67: Optional: reduce redundant rebuilds by only notifying on actual state changes.

setCurrentCenter, setCurrentZoom, setLoading, etc. always notifyListeners(). Consider early-return when the value is unchanged (especially if map emits frequent camera updates).

lib/widgets/map_widgets/tree_map_widgets.dart (1)

7-45: Nice: marker widgets are simple and composable; consider Semantics / larger tap target.

If these are used inside map markers, consider giving the container a fixed width/height (e.g., 36–44) and a Semantics(label: ...) for accessibility/screen readers.

Also applies to: 47-72

📜 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 1ad5509.

📒 Files selected for processing (7)
  • lib/main.dart (4 hunks)
  • lib/pages/map_view_page.dart (1 hunks)
  • lib/providers/map_provider.dart (1 hunks)
  • lib/utils/constants/bottom_nav_constants.dart (1 hunks)
  • lib/utils/constants/route_constants.dart (1 hunks)
  • lib/utils/services/tree_map_service.dart (1 hunks)
  • lib/widgets/map_widgets/tree_map_widgets.dart (1 hunks)
🔇 Additional comments (5)
lib/utils/services/tree_map_service.dart (2)

165-171: Verify decodeGeohash output order matches GeoHasher.decode.

The decodeGeohash method assumes decoded[0] is latitude and decoded[1] is longitude. Given that encode takes (longitude, latitude), the decode output order should be verified to ensure consistency.


81-82: Parameter order is correct. The dart_geohash library's GeoHasher.encode method expects (longitude, latitude) order, and the code correctly uses this sequence.

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

32-37: LGTM!

The new Map navigation item is correctly implemented with appropriate icons and route reference, consistent with the existing navigation items pattern.

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

4-4: LGTM!

The route constants follow the established pattern in this file, maintaining consistency with existing route definitions.

Also applies to: 12-12

lib/main.dart (1)

6-6: LGTM!

The map view integration is well-structured:

  • Route definition follows the existing pattern
  • MapProvider is correctly registered in the MultiProvider
  • Imports are properly organized

Also applies to: 21-21, 157-163, 177-177

- Fix longitude conversion to use correct offset (180 vs 90)
- Replace inaccurate custom Math class with dart:math library
- Split _convertCoordinate into separate lat/lng functions
- Add null guards for getThemeColors to prevent runtime errors
- Remove 88 lines of unreliable Taylor series approximations

This fixes critical bugs in distance calculations and coordinate display.
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: 3

♻️ Duplicate comments (1)
lib/widgets/map_widgets/tree_map_widgets.dart (1)

127-150: Remove the extra vertical gap when hasUserLocation == false (unconditional SizedBox).

-        // Center on user location
-        if (hasUserLocation)
-          Container(
+        // Center on user location
+        if (hasUserLocation) ...[
+          Container(
             decoration: BoxDecoration(
               color: Colors.white,
               borderRadius: BorderRadius.circular(8),
               boxShadow: [
                 BoxShadow(
                   color: Colors.black.withValues(alpha: 0.2),
                   blurRadius: 4,
                   offset: const Offset(0, 2),
                 ),
               ],
             ),
             child: IconButton(
               icon: const Icon(Icons.my_location, color: Colors.blue),
               onPressed: onCenterUser,
               tooltip: 'Center on Me',
             ),
           ),
-        const SizedBox(height: 8),
+          const SizedBox(height: 8),
+        ],
🧹 Nitpick comments (1)
lib/widgets/map_widgets/tree_map_widgets.dart (1)

196-205: Consider deduplicating lat/lng decoding (service + card implement the same codec).

A small shared utility (e.g., CoordinateCodec.decodeLatitudeMicro(int) / decodeLongitudeMicro(int)) reduces drift risk.

Also applies to: 279-284

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2050bfd and 1222e37.

📒 Files selected for processing (3)
  • lib/pages/map_view_page.dart (1 hunks)
  • lib/utils/services/tree_map_service.dart (1 hunks)
  • lib/widgets/map_widgets/tree_map_widgets.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/pages/map_view_page.dart
🔇 Additional comments (2)
lib/utils/services/tree_map_service.dart (1)

132-153: Core geo helpers look solid (haversine + correct lat/lng fixed-point decoding + prefix match).

Also applies to: 199-209, 211-234

lib/widgets/map_widgets/tree_map_widgets.dart (1)

151-178: Theme color fallback looks good; loading/disabled states are wired correctly.

Also applies to: 302-313

- Fix planting_proposals_tab.dart: was missing offsets entirely
- Fix recent_trees_widget.dart: was using lat offset for longitude
- Ensures all tree locations display correctly in Trees list and proposals
- Brings all coordinate conversions in line with blockchain encoding

This fixes coordinate display bugs in non-map components that would show
trees at incorrect GPS locations by 90-180 degrees.
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: 1

♻️ Duplicate comments (1)
lib/widgets/nft_display_utils/recent_trees_widget.dart (1)

96-106: Duplicate coordinate conversion logic.

These functions are identical to those in planting_proposals_tab.dart (lines 122-132). Please extract them to a shared utility module as suggested in the review comment for that file.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1222e37 and 26bc602.

📒 Files selected for processing (2)
  • lib/widgets/nft_display_utils/recent_trees_widget.dart (2 hunks)
  • lib/widgets/organisation_details_page/tabs/planting_proposals_tab.dart (2 hunks)
🔇 Additional comments (2)
lib/widgets/organisation_details_page/tabs/planting_proposals_tab.dart (1)

210-211: LGTM!

The coordinate conversion usage is correct, and displaying 6 decimal places provides appropriate precision (~0.11 meters) for tree locations.

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

263-263: LGTM!

The coordinate display is correctly implemented with appropriate precision.

- Fix map tree details navigation: /tree-details -> /trees to match router
- Update fetch limits to respect maxTrees parameter in both bounding box and geohash queries
- Ensures at least maxTrees can be returned after filtering (was hard-capped at 50)
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: 4

♻️ Duplicate comments (2)
lib/utils/services/tree_map_service.dart (1)

22-24: Don’t log precise location bounds (PII) in app logs.

Those debug logs can leak sensitive user location context if collected remotely; sanitize (round/coarsen) or gate behind debug-only.

+import 'package:flutter/foundation.dart';
 ...
-      logger.d("Fetching trees in bounding box: "
-          "lat[$minLat, $maxLat], lng[$minLng, $maxLng]");
+      if (!kReleaseMode) {
+        logger.d("Fetching trees in bounding box");
+      }
lib/pages/map_view_page.dart (1)

23-34: Dispose MapController in dispose().

 class _MapViewPageState extends State<MapViewPage> {
   final MapController _mapController = MapController();
   Tree? _selectedTree;
   bool _isInitialized = false;
@@
   void initState() {
@@
   }
+
+  @override
+  void dispose() {
+    _mapController.dispose();
+    super.dispose();
+  }
🧹 Nitpick comments (3)
lib/utils/services/tree_map_service.dart (1)

176-197: Harden _parseTreeFromMap against partial/null contract fields.

Casting treeData['species'] as String (etc.) will throw if the field is null/missing; you catch and skip the tree, which can amplify “no trees found” for minor data issues. Consider defaults or more targeted validation.

lib/pages/map_view_page.dart (2)

147-155: Deduplicate coordinate conversion helpers.

_convertLatitude/_convertLongitude are duplicated here and in TreeMapService; consider a shared util (or re-use TreeMapService helpers) to avoid future drift.

Also applies to: 194-207


100-110: Use MapEventMoveEnd instead of MapEventMove to avoid excessive Provider updates.

MapEventMove fires continuously while dragging the map, causing setCurrentCenter/Zoom to notify listeners on every event and potentially causing jank. Prefer MapEventMoveEnd, which fires once when the drag completes. If you need live updates during dragging, debounce MapEventMove with a Timer (300ms duration is typical) or use rxdart.debounceTime().

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26bc602 and 73c25af.

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

Comment on lines +36 to +57
Future<void> _initializeMap() async {
if (_isInitialized) return;

final mapProvider = Provider.of<MapProvider>(context, listen: false);

try {
// Get user location
final location = await LocationService().getCurrentLocation();
if (location.isValid) {
mapProvider.setUserLocation(LatLng(location.latitude!, location.longitude!));
logger.d("User location set: ${location.latitude}, ${location.longitude}");
}

_isInitialized = true;

// Load initial trees
await _loadTreesInArea();
} catch (e) {
logger.e("Error initializing map: $e");
mapProvider.setError("Failed to initialize map: $e");
}
}
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

Avoid requesting precise location before the user opts in / wallet is connected.

Right now _initializeMap() calls getCurrentLocation() even when you’re rendering the “Connect Your Wallet” prompt; this can trigger permissions unexpectedly and is privacy-sensitive.

 Future<void> _initializeMap() async {
   if (_isInitialized) return;
 
   final mapProvider = Provider.of<MapProvider>(context, listen: false);
+  final walletProvider = Provider.of<WalletProvider>(context, listen: false);
 
   try {
+    if (!walletProvider.isConnected) {
+      _isInitialized = true;
+      return;
+    }
     // Get user location
     final location = await LocationService().getCurrentLocation();

Also consider gating the log line that prints exact lat/lng.

Also applies to: 63-66

Comment on lines +46 to +47
logger.d("User location set: ${location.latitude}, ${location.longitude}");
}
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

Don’t log exact user location / bounds in production logs.

(At minimum, gate behind debug-only or remove the coordinates.)

Also applies to: 74-75

🤖 Prompt for AI Agents
In lib/pages/map_view_page.dart around lines 46-47 (and similarly at 74-75) the
code logs exact user latitude/longitude which must not be emitted in production;
either remove the coordinate values from logs or gate the logs behind a
debug-only check (e.g., use Flutter's kDebugMode) so they only run in
development, or else redact/round the coordinates to coarse values (city-level
or use buckets) before logging; update the logging calls accordingly and add the
necessary import for the debug constant if you choose the kDebugMode guard.

Comment on lines +25 to +31
// Fetch a large batch of recent trees
// In future, this could be optimized with backend or contract-level filtering
final result = await ContractReadFunctions.getRecentTreesPaginated(
walletProvider: walletProvider,
offset: 0,
limit: math.max(50, maxTrees), // Fetch enough trees to satisfy maxTrees after filtering
);
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

Single “recent trees” page can miss nearby trees; consider paging until enough matches.

Right now you only fetch offset: 0 and filter client-side; if relevant trees aren’t in the first page, the map may show “no trees” even when they exist nearby.

-      final result = await ContractReadFunctions.getRecentTreesPaginated(
-        walletProvider: walletProvider,
-        offset: 0,
-        limit: math.max(50, maxTrees),
-      );
+      final int pageSize = math.max(50, maxTrees);
+      final List<dynamic> treesData = [];
+      for (var offset = 0; treesData.length < pageSize * 10; offset += pageSize) {
+        final result = await ContractReadFunctions.getRecentTreesPaginated(
+          walletProvider: walletProvider,
+          offset: offset,
+          limit: pageSize,
+        );
+        if (!result.success || result.data == null) break;
+        final page = (result.data['trees'] ?? []) as List<dynamic>;
+        if (page.isEmpty) break;
+        treesData.addAll(page);
+        // Optionally: early-exit if you already found enough matches while parsing/filtering.
+      }

(Apply similarly in getTreesNearLocation.)

Also applies to: 92-98

Comment on lines +155 to +172
/// Get geohash for a coordinate
static String getGeohash(double latitude, double longitude, {int precision = 6}) {
return _geoHasher.encode(longitude, latitude, precision: precision);
}

/// Get neighboring geohashes
static List<String> getNeighboringGeohashes(String geohash) {
return _geoHasher.neighbors(geohash).values.toList();
}

/// Decode geohash to coordinates
static Map<String, double> decodeGeohash(String geohash) {
final decoded = _geoHasher.decode(geohash);
return {
'latitude': decoded[0],
'longitude': decoded[1],
};
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd pubspec.yaml -type f

Repository: StabilityNexus/Treee

Length of output: 234


🏁 Script executed:

grep -A 20 "dart_geohash" pubspec.yaml 2>/dev/null | head -30

Repository: StabilityNexus/Treee

Length of output: 711


🌐 Web query:

dart_geohash 2.1.0 GeoHasher encode decode parameter order coordinate order documentation

💡 Result:

In dart_geohash (including v2.1.0) GeoHasher.encode expects (longitude, latitude) — i.e. lon first, lat second. The package examples and API docs show calls like geoHasher.encode(-98, 38) and decode returns [longitude, latitude]. [1][2]

Sources:
[1] dart_geohash package page (examples).
[2] dart_geohash example / API docs (encode/decode usage).


Fix inverted coordinate order in decodeGeohash().

The dart_geohash library's decode() method returns [longitude, latitude], but the code assigns them backwards:

'latitude': decoded[0],  // decoded[0] is longitude
'longitude': decoded[1],  // decoded[1] is latitude

This silently swaps coordinates and will misplace all markers. Correct to:

'latitude': decoded[1],
'longitude': decoded[0],
🤖 Prompt for AI Agents
In lib/utils/services/tree_map_service.dart around lines 155 to 172 the
decodeGeohash method assigns decoded values in the wrong order:
dart_geohash.decode() returns [longitude, latitude] but the code maps decoded[0]
to 'latitude' and decoded[1] to 'longitude'. Swap the assignments so 'latitude'
gets decoded[1] and 'longitude' gets decoded[0], keeping the return map keys the
same.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dynamic map that renders nearby planted trees

1 participant