Google Maps / Custom Locations / Custom Theme / External URL public Image

Collaboration & Networking

Hey All,

Figured I would share this as I couldn't find anything really out there for my use case. I wanted to use icons that were from a public URL. I have uploaded my images to an external storage provider - Azure and have managed to create the following script that works within the test environment and for the users having trouble with assets in FF.

Please note: if you don't see your image load in the run and test environment, this is most likely because of the image CORS or access permissions. You will have to tweak your fire storage rules to allow for public access from all browsers, which i wouldn't recommended if you are not familiar with this. You can use other services like azure, one drive, drop box ect. Also, make sure you add the FF google maps to a blank page, you don't need to use it, but this will help with the connection of the custom map and setup the API keys in the settings.

Hopefully this will save someone hours of headache.

// Automatic FlutterFlow imports
import '/backend/backend.dart';
import '/backend/schema/structs/index.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/widgets/index.dart'; // Imports other custom widgets
import '/custom_code/actions/index.dart'; // Imports custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom widget code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

// ignore: unnecessary_import

import 'package:google_maps_flutter/google_maps_flutter.dart'
    as google_maps_flutter;
import '/flutter_flow/lat_lng.dart' as latlng;
import 'dart:async';
export 'dart:async' show Completer;
export 'package:google_maps_flutter/google_maps_flutter.dart' hide LatLng;
export '/flutter_flow/lat_lng.dart' show LatLng;
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
import 'dart:ui';
// ignore: unnecessary_import
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:typed_data';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/http.dart' as http;
import 'package:image/image.dart' as img;

class FirestoreMap extends StatefulWidget {
  const FirestoreMap({
    super.key,
    this.width,
    this.height,
    this.places,
    required this.centerLatitude,
    required this.centerLongitude,
    required this.showLocation,
    required this.showCompass,
    required this.showMapToolbar,
    required this.showTraffic,
    required this.allowZoom,
    required this.showZoomControls,
    required this.defaultZoom,
    this.mapStyleJson,
    this.onClickMarker,
  });

  final double? width;
  final double? height;
  final List<LocationsRecord>? places;
  final double centerLatitude;
  final double centerLongitude;
  final bool showLocation;
  final bool showCompass;
  final bool showMapToolbar;
  final bool showTraffic;
  final bool allowZoom;
  final bool showZoomControls;
  final double defaultZoom;
  final Future Function(LocationsRecord? placeRow)? onClickMarker;
  final String? mapStyleJson;

  @override
  State<FirestoreMap> createState() => _FirestoreMapState();
}

class _FirestoreMapState extends State<FirestoreMap> {
  Completer<google_maps_flutter.GoogleMapController> _controller = Completer();
  Map<String, google_maps_flutter.BitmapDescriptor> _customIcons = {};
  Set<google_maps_flutter.Marker> _markers = {};
  // ignore: unused_field
  List<LocationsRecord> _list = [];
  // ignore: unused_field
  StreamSubscription? _locationsSubscription;

  late google_maps_flutter.LatLng _center;

  @override
  void initState() {
    super.initState();
    _center = google_maps_flutter.LatLng(
        widget.centerLatitude, widget.centerLongitude);
    _loadMarkerIcons().then((_) {
      _listenToLocations();
    });
  }

  void _listenToLocations() {
    _locationsSubscription = FirebaseFirestore.instance
        .collection('locations')
        .snapshots()
        .listen((snapshot) {
      final locations = snapshot.docs
          .map((doc) => LocationsRecord.fromSnapshot(doc))
          .toList();
      setState(() {
        _list = locations;
        // Removed the call to _loadMarkerIcons()
      });
    });
  }

  Future<void> _loadMarkerIcons() async {
    Set<String> uniqueIconPaths =
        widget.places?.map((data) => data.coverImage).toSet() ?? {};

    for (String urlString in uniqueIconPaths) {
      if (urlString.isNotEmpty) {
        // Fetch the bytes from the network URL using http package
        final response = await http.get(Uri.parse(urlString));
        if (response.statusCode == 200) {
          // Decode the image from the bytes
          img.Image? image = img.decodeImage(response.bodyBytes);
          if (image != null) {
            // Resize the image
            img.Image thumbnail =
                img.copyResize(image, width: 30); // Set the desired size

            // Create the BitmapDescriptor from resized bytes
            final descriptor = google_maps_flutter.BitmapDescriptor.fromBytes(
              img.encodePng(thumbnail),
            );

            // Store the descriptor with the URL as the key for later use with markers
            _customIcons[urlString] = descriptor;
          } else {
            print('Failed to decode image at $urlString.');
          }
        } else {
          // Handle the case when the image is not retrieved from the network
          print(
              'Failed to load image at $urlString, status code: ${response.statusCode}');
        }
      }
    }
    _updateMarkers(); // Update markers once icons are loaded
  }

  void _updateMarkers() {
    setState(() {
      _markers = _createMarkers();
    });
  }

  void _onMapCreated(google_maps_flutter.GoogleMapController controller) {
    _controller.complete(controller);
    if (widget.mapStyleJson != null) {
      // Apply the map style if provided
      controller.setMapStyle(widget.mapStyleJson);
    }
  }

  Set<google_maps_flutter.Marker> _createMarkers() {
    var tmp = <google_maps_flutter.Marker>{};
    for (int i = 0; i < (widget.places ?? []).length; i++) {
      var place = widget.places?[i];
      final latlng.LatLng coordinates = latlng.LatLng(
          place?.tempAddress?.latitude ?? 0.0,
          place?.tempAddress?.longitude ?? 0.0);
      final google_maps_flutter.LatLng googleMapsLatLng =
          google_maps_flutter.LatLng(
              coordinates.latitude, coordinates.longitude);
      google_maps_flutter.BitmapDescriptor icon =
          _customIcons[place?.coverImage] ??
              google_maps_flutter.BitmapDescriptor.defaultMarker;

      final google_maps_flutter.Marker marker = google_maps_flutter.Marker(
        markerId: google_maps_flutter.MarkerId('${place?.name ?? "Marker"}_$i'),
        position: googleMapsLatLng,
        icon: icon,
        infoWindow: google_maps_flutter.InfoWindow(
            title: place?.name, snippet: place?.description),
        onTap: () async {
          // Animate camera to the marker's position when tapped
          final google_maps_flutter.GoogleMapController controller =
              await _controller.future;
          controller
              .animateCamera(google_maps_flutter.CameraUpdate.newCameraPosition(
            google_maps_flutter.CameraPosition(
              target: googleMapsLatLng,
              zoom: 15.0, // Adjust zoom level as needed
            ),
          ));
          // Execute additional callback if provided
          final callback = widget.onClickMarker;
          if (callback != null) {
            await callback(place);
          }
        },
      );

      tmp.add(marker);
    }
    return tmp;
  }

  @override
  Widget build(BuildContext context) {
    return google_maps_flutter.GoogleMap(
      onMapCreated: _onMapCreated,
      onCameraMove: (cameraPosition) {
        // This callback is triggered on every camera move
        // If you need to update the map based on camera movement, add your logic here
      },
      onCameraIdle: () {
        // This callback is triggered when the camera stops moving
        // You can fetch and update locations here if necessary
      },
      zoomGesturesEnabled: widget.allowZoom,
      zoomControlsEnabled: widget.showZoomControls,
      myLocationEnabled: widget.showLocation,
      myLocationButtonEnabled: true,
      compassEnabled: widget.showCompass,
      mapToolbarEnabled: widget.showMapToolbar,
      mapType: google_maps_flutter.MapType.normal,
      trafficEnabled: widget.showTraffic,
      initialCameraPosition: google_maps_flutter.CameraPosition(
        target: _center,
        zoom: widget.defaultZoom,
      ),
      markers: _markers,
    );
  }
}
2