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

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages