Skip to content

TBR-Group-software/flutter_maps_app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

About the project

Maps is a open-source, clean-architecture Flutter-based app designed to research nearby places by a specific category.

Key Features

  • Interactive 3D map screen built with Mapbox.
  • Automatic current-location detection on app start.
  • Category-based place search (Restaurants, Cafés, Hotels, Petrol Stations, ATMs, GYMs, Pharmacies).
  • Yelp API integration to fetch nearby businesses by selected category.
  • Overpass / OpenStreetMap integration to fetch nearby building polygons for those businesses.
  • Building highlighting on the map for matched results, plus tap-to-select behavior.
  • Business details bottom sheet on tap (name, image, address, rating, phone, category).
  • Category picker bottom sheet with persisted selection (HydratedBloc keeps selected category across launches).

Project Structure

maps/
├── android/                # Android platform files
├── ios/                    # iOS platform files
├── lib/
│   ├── backbone/           # DI
│   ├── data/               # Data layer
│   ├── domain/             # Business logic
│   └── presentation/       # UI layer
├── environment/.env        # Environment variables
├── pubspec.yaml            # Dependencies
└── README.md               # Introduction

Presentation Layer

  • Responsibility: Handles all UI and user interaction logic.
  • Components:
    • Widgets/Pages: Flutter widgets that form the screens of the app. They are responsible for rendering the UI based on the current state.
    • BLoC (Business Logic Component): Manages the state of a feature. It listens to events from the UI (e.g., button clicks) and emits new states in response. It communicates with the Domain layer via Use Cases.

Domain Layer

  • Responsibility: Contains the core business logic of the application. This layer is completely independent of the UI and data sources.
  • Components:
    • Entities: Business objects that represent the core data structures of the app (e.g., BusinessResponse).
    • Gateways (Abstract): Defines the contracts (interfaces) for the Data layer to implement. This decouples the domain logic from the specific data source implementations.
    • Use Cases: Encapsulates a single, specific business rule. They orchestrate the flow of data between the Presentation and Data layers by using repository contracts.

Data Layer

  • Responsibility: Responsible for retrieving data from various sources (e.g., remote API, local database).
  • Components:
    • Models: Data Transfer Objects (DTOs) that are specific to a data source (e.g., BusinessResponseDto for the Yelp API). They include logic for serialization/deserialization (fromJson/toJson).
    • Gateways (Implementation): Implements the repository contracts defined in the Domain layer.

Code Samples

Highlighting buildings on the map

When a building is pressable, its fill color changes to yellow to indicate that it can be selected.

  Future<void> _highlightBusinessesOnMap({
    required List<Map<String, dynamic>> features,
  }) async {
    if (_mapboxMap == null || !_styleLoaded) return;

    // clear previous highlights
    await _clearAllHighlights();

    // add new highlights
    for (final feature in features) {
      final props = feature['properties'] as Map<String, dynamic>;
      final id = props['id'];

      await _setFeatureState(id: id.toString(), shouldHighlight: true);
      _highlightedFeatureIds.add(id.toString());
    }
  }

  Future<void> _clearAllHighlights() async {
    if (_mapboxMap == null || !_styleLoaded) return;

    try {
      if (_lastFeatureId != null) {
        await _mapboxMap!.setFeatureState(
          AppConstants.sourceId,
          AppConstants.sourceLayerId,
          _lastFeatureId!,
          jsonEncode({'selected': false}),
        );
        _lastFeatureId = null;
      }

      if (_highlightedFeatureIds.isNotEmpty) {
        for (final id in _highlightedFeatureIds) {
          await _setFeatureState(id: id, shouldHighlight: false);
        }
        _highlightedFeatureIds.clear();
      }
    } catch (_) {
      // ignore if the user taps too fast during map style reload
    }
  }

Future<void> _setFeatureState({
    required String id,
    required bool shouldHighlight,
  }) async {
    await _mapboxMap?.setFeatureState(
      AppConstants.sourceId,
      AppConstants.sourceLayerId,
      id,
      jsonEncode({'categoryHighlight': shouldHighlight}),
    );
  }

Building Polygons

Fetching building geometry from Overpass (OpenStreetMap) for a list of businesses, and returning it as GeoJSON-like features.

class OverpassGatewayImpl implements OverpassGateway {
  @override
  Future<List<Map<String, dynamic>>> getFeatures({
    required List<BusinessDto> businesses,
  }) async {
    // Initialize a string buffer to build the Overpass API query
    final buffer = StringBuffer();
    buffer.write('[out:json];(');

    // Loop through each business and add queries for nearby buildings
    for (final b in businesses) {
      final lat = b.coordinates.latitude;
      final lon = b.coordinates.longitude;
      // Query ways (building polygons) within 50 meters of the business
      buffer.write('way(around:30,$lat,$lon)["building"];');
      // Query relations (complex polygons like multipolygon) within 50 meters
      buffer.write('relation(around:30,$lat,$lon)["building"];');
    }

    buffer.write(');(._;>;);out;');

    // Build the Overpass API URL with the encoded query
    final url =
        'https://overpass-api.de/api/interpreter?data=${Uri.encodeComponent(buffer.toString())}';
    final response = await http.get(Uri.parse(url));

    // Check if the response is successful
    if (response.statusCode != 200) {
      debugPrint('Overpass error.');
      return [];
    }

    // Decode the JSON response from Overpass API
    final data = jsonDecode(response.body) as Map<String, dynamic>;

    // Separate elements into nodes, ways, and relations
    final nodes = <int, List<double>>{};
    final ways = <int, List<int>>{};
    final relations = <int, List<int>>{};

    for (final Map<String, dynamic> el in data['elements']) {
      if (el['type'] == 'node') {
        // Store node coordinates (longitude, latitude)
        nodes[el['id']] = [el['lon'], el['lat']];
      } else if (el['type'] == 'way') {
        // Store way nodes (list of node IDs)
        ways[el['id']] = List<int>.from(el['nodes']);
      } else if (el['type'] == 'relation') {
        // For relations, store references to way IDs
        final members = el['members'] as List;
        final wayIds = members
            .where((m) => m['type'] == 'way')
            .map<int>((m) => m['ref'] as int)
            .toList();
        relations[el['id']] = wayIds;
      }
    }

    final features = <Map<String, dynamic>>[];

    // Process way-buildings into GeoJSON polygons
    for (final entry in ways.entries) {
      final coords = entry.value.map((id) => nodes[id]!).toList();
      if (coords.isEmpty) continue;

      // Close the polygon if first and last points are not the same
      if (coords.first[0] != coords.last[0] ||
          coords.first[1] != coords.last[1]) {
        coords.add(coords.first);
      }

      // Add the polygon as a GeoJSON Feature
      features.add({
        'type': 'Feature',
        'geometry': {
          'type': 'Polygon',
          'coordinates': [coords],
        },
        'properties': {'id': entry.key},
      });
    }

    // Process multipolygon (relations)
    for (final entry in relations.entries) {
      final polygons = <List<List<double>>>[];

      for (final wayId in entry.value) {
        if (!ways.containsKey(wayId)) continue;
        final coords = ways[wayId]!.map((id) => nodes[id]!).toList();
        if (coords.isEmpty) continue;

        // Close the polygon
        if (coords.first[0] != coords.last[0] ||
            coords.first[1] != coords.last[1]) {
          coords.add(coords.first);
        }

        polygons.add(coords);
      }

      if (polygons.isNotEmpty) {
        // Add the multipolygon as a GeoJSON Feature
        features.add({
          'type': 'Feature',
          'geometry': {
            'type': 'MultiPolygon',
            'coordinates': [polygons],
          },
          'properties': {'id': entry.key},
        });
      }
    }

    return features;
  }
}

Build with

Prerequisites

Before running this project, make sure you have:

  • Flutter SDK (3.9.0 or higher)
  • Dart SDK (included with Flutter)
  • Android Studio / Xcode (for mobile development)
  • YELP API Key (get one from Yelp)
  • Mapbox API Key (get one from Mapbox)

Installation

  1. Clone the repository

    git clone https://github.com/yourusername/flutter_maps_app.git
    cd maps
  2. Install dependencies

    flutter pub get
  3. Set up environment variables

    Create a environment/.env file in the root directory and adjust with your API KEY:

    MAPBOX_ACCESS_TOKEN=your_mapbox_api_key_here
    YELP_API_KEY=your_yelp_api_key_here
    MAP_STYLE=your_custom_or_default_map_style_path_here
  4. Run the application

    flutter run

Platform Setup

Android

  • Minimum SDK: 21
  • Target SDK: Latest
  • Permissions: Location

iOS

  • iOS 14.0+
  • Permissions: Location

Usage

  1. Category selection: Open the category bottom sheet and select any
  2. Building selection: Once the highlighted buildings loaded with yellow color, select any building to see its basic information

About

No description or website provided.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages