diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/GlobalMethodHandler.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/GlobalMethodHandler.java index edb8d2ee2..2dd3d7d31 100644 --- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/GlobalMethodHandler.java +++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/GlobalMethodHandler.java @@ -28,6 +28,7 @@ class GlobalMethodHandler implements MethodChannel.MethodCallHandler { @NonNull private final BinaryMessenger messenger; @Nullable private FlutterPlugin.FlutterAssets flutterAssets; @Nullable private OfflineChannelHandlerImpl downloadOfflineRegionChannelHandler; + @Nullable private MapSnapshotWrapper mapSnapshotter; GlobalMethodHandler(@NonNull FlutterPlugin.FlutterPluginBinding binding) { @@ -125,6 +126,13 @@ public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { OfflineManagerUtils.deleteRegion( result, context, methodCall.argument("id").longValue()); break; + case "startMapSnapshot": + Map snapshotArgs = (Map) methodCall.arguments; + if (mapSnapshotter == null) { + mapSnapshotter = new MapSnapshotWrapper(new MethodChannel(messenger, "plugins.flutter.io/maplibre_gl"), context); + } + mapSnapshotter.startSnapshot(snapshotArgs, result); + break; default: result.notImplemented(); break; diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapSnapshotWrapper.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapSnapshotWrapper.java new file mode 100644 index 000000000..468835217 --- /dev/null +++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapSnapshotWrapper.java @@ -0,0 +1,181 @@ +package org.maplibre.maplibregl; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.MethodChannel; +import org.maplibre.android.snapshotter.MapSnapshot; +import org.maplibre.android.snapshotter.MapSnapshotter; +import org.maplibre.android.camera.CameraPosition; +import org.maplibre.android.geometry.LatLng; +import org.maplibre.android.maps.MapLibreMap; +import org.maplibre.android.maps.Style; +import org.maplibre.android.style.layers.PropertyValue; + +import java.io.ByteArrayOutputStream; +import java.util.List; +import java.util.Map; + +public class MapSnapshotWrapper { + private static final String TAG = MapSnapshotWrapper.class.getSimpleName(); + + private final MethodChannel channel; + private final android.content.Context context; + private MapSnapshotter snapshotter; + private MethodChannel.Result result; + + public MapSnapshotWrapper(MethodChannel channel, android.content.Context context) { + this.channel = channel; + this.context = context; + } + + public void startSnapshot(Map arguments, MethodChannel.Result result) { + this.result = result; + + try { + // Parse arguments + Integer width = (Integer) arguments.get("width"); + Integer height = (Integer) arguments.get("height"); + String styleUrl = (String) arguments.get("styleUrl"); + Map cameraPositionMap = (Map) arguments.get("cameraPosition"); + + if (width == null || height == null || styleUrl == null || cameraPositionMap == null) { + result.error("INVALID_ARGUMENTS", "Missing required arguments", null); + return; + } + + // Create snapshot options + MapSnapshotter.Options options = new MapSnapshotter.Options(width, height) + .withStyle(styleUrl); + + // Configure camera + if (cameraPositionMap.containsKey("target")) { + Map target = (Map) cameraPositionMap.get("target"); + if (target != null) { + Double lat = (Double) target.get("latitude"); + Double lng = (Double) target.get("longitude"); + if (lat != null && lng != null) { + options.withCameraPosition(new CameraPosition.Builder() + .target(new LatLng(lat, lng)) + .build()); + } + } + } + + if (cameraPositionMap.containsKey("zoom")) { + Double zoom = (Double) cameraPositionMap.get("zoom"); + if (zoom != null) { + CameraPosition.Builder cameraBuilder = new CameraPosition.Builder(); + if (options.getCameraPosition() != null) { + cameraBuilder.target(options.getCameraPosition().target); + } + CameraPosition cameraPosition = cameraBuilder.zoom(zoom).build(); + Log.d(TAG, "Setting camera zoom to: " + zoom); + options.withCameraPosition(cameraPosition); + } + } + + // Create snapshotter + snapshotter = new MapSnapshotter(context, options); + + // Set up snapshot listener + snapshotter.start(new MapSnapshotter.SnapshotReadyCallback() { + @Override + public void onSnapshotReady(@NonNull MapSnapshot snapshot) { + try { + // Get the bitmap + Bitmap bitmap = snapshot.getBitmap(); + + // Add markers if provided + List> markers = (List>) arguments.get("markers"); + if (markers != null && !markers.isEmpty()) { + bitmap = addMarkersToBitmap(bitmap, markers, snapshot); + } + + // Convert to PNG byte array + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + byte[] imageData = stream.toByteArray(); + + // Return success result + result.success(imageData); + } catch (Exception e) { + Log.e(TAG, "Error processing snapshot", e); + result.error("SNAPSHOT_ERROR", "Failed to process snapshot: " + e.getMessage(), null); + } + } + }); + + } catch (Exception e) { + Log.e(TAG, "Error starting snapshot", e); + result.error("SNAPSHOT_ERROR", "Failed to start snapshot: " + e.getMessage(), null); + } + } + + private Bitmap addMarkersToBitmap(Bitmap originalBitmap, List> markers, MapSnapshot snapshot) { + // Create a mutable copy of the original bitmap + Bitmap.Config config = originalBitmap.getConfig(); + if (config == null) { + config = Bitmap.Config.ARGB_8888; + } + Bitmap resultBitmap = originalBitmap.copy(config, true); + + Canvas canvas = new Canvas(resultBitmap); + Paint paint = new Paint(); + + for (Map marker : markers) { + try { + // Parse marker data + Map position = (Map) marker.get("position"); + Double lat = (Double) position.get("latitude"); + Double lng = (Double) position.get("longitude"); + byte[] iconData = (byte[]) marker.get("iconData"); + Double iconSize = (Double) marker.get("iconSize"); + + if (lat == null || lng == null || iconData == null || iconSize == null) { + continue; + } + + // Convert coordinates to screen point + LatLng markerLatLng = new LatLng(lat, lng); + PointF point = snapshot.pixelForLatLng(markerLatLng); + + // Create icon bitmap + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap iconBitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.length, options); + + if (iconBitmap != null) { + Log.d(TAG, "Android bitmap size: " + iconBitmap.getWidth() + "x" + iconBitmap.getHeight()); + // Use original bitmap size (already includes DPR from Flutter) + int scaledWidth = iconBitmap.getWidth(); + int scaledHeight = iconBitmap.getHeight(); + Log.d(TAG, "Android marker - using original bitmap size: " + scaledWidth + "x" + scaledHeight); + Bitmap scaledIcon = iconBitmap; // Use original bitmap directly + + // Draw icon centered at the point + float left = point.x - scaledWidth / 2; + float top = point.y - scaledHeight; + canvas.drawBitmap(scaledIcon, left, top, paint); + + // Only recycle scaledIcon if it's a different bitmap + if (scaledIcon != iconBitmap) { + scaledIcon.recycle(); + } + iconBitmap.recycle(); + } + + } catch (Exception e) { + Log.e(TAG, "Error drawing marker", e); + } + } + + return resultBitmap; + } +} \ No newline at end of file diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapsPlugin.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapsPlugin.swift index 802e4790f..f8fd57685 100644 --- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapsPlugin.swift +++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapsPlugin.swift @@ -5,6 +5,7 @@ import UIKit public class MapLibreMapsPlugin: NSObject, FlutterPlugin { static var downloadOfflineRegionChannelHandler: OfflineChannelHandler? = nil + static var mapSnapshotter: MapSnapshotter? = nil public static func register(with registrar: FlutterPluginRegistrar) { let instance = MapLibreMapFactory(withRegistrar: registrar) @@ -109,6 +110,20 @@ public class MapLibreMapsPlugin: NSObject, FlutterPlugin { return } OfflineManagerUtils.deleteRegion(result: result, id: id) + case "startMapSnapshot": + guard let args = methodCall.arguments as? [String: Any] else { + result(FlutterError( + code: "INVALID_ARGUMENTS", + message: "Invalid arguments for startMapSnapshot", + details: nil + )) + return + } + + if mapSnapshotter == nil { + mapSnapshotter = MapSnapshotter(channel: channel) + } + mapSnapshotter?.startSnapshot(arguments: args, result: result) default: result(FlutterMethodNotImplemented) } diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapSnapshotter.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapSnapshotter.swift new file mode 100644 index 000000000..bd894fe45 --- /dev/null +++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapSnapshotter.swift @@ -0,0 +1,136 @@ +import Flutter +import Foundation +import MapLibre +import UIKit + +public class MapSnapshotter: NSObject { + private var snapshotter: MLNMapSnapshotter? + private var result: FlutterResult? + private var channel: FlutterMethodChannel? + + public init(channel: FlutterMethodChannel) { + self.channel = channel + super.init() + } + + public func startSnapshot( + arguments: [String: Any], + result: @escaping FlutterResult + ) { + self.result = result + + guard let width = arguments["width"] as? Int, + let height = arguments["height"] as? Int, + let styleUrl = arguments["styleUrl"] as? String, + let cameraPosition = arguments["cameraPosition"] as? [String: Any] + else { + result(FlutterError( + code: "INVALID_ARGUMENTS", + message: "Missing required arguments", + details: nil + )) + return + } + + let size = CGSize(width: width, height: height) + let camera = MLNMapCamera(lookingAtCenter: CLLocationCoordinate2D(latitude: 0, longitude: 0), fromDistance: 1000, pitch: 0, heading: 0) + let options = MLNMapSnapshotOptions(styleURL: URL(string: styleUrl)!, camera: camera, size: size) + + // Configure camera + if let zoom = cameraPosition["zoom"] as? Double { + print("Setting zoom level to: \(zoom)") + options.zoomLevel = zoom + } + + if let center = cameraPosition["target"] as? [String: Double], + let lat = center["latitude"], + let lng = center["longitude"] { + options.camera.centerCoordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng) + } + + // Create snapshotter + snapshotter = MLNMapSnapshotter(options: options) + + // Start snapshot + snapshotter?.start { (snapshot, error) in + if let error = error { + self.result?(FlutterError( + code: "SNAPSHOT_ERROR", + message: error.localizedDescription, + details: nil + )) + return + } + + guard let snapshot = snapshot else { + self.result?(FlutterError( + code: "SNAPSHOT_ERROR", + message: "Failed to generate snapshot", + details: nil + )) + return + } + + // Convert snapshot to image data + let image = snapshot.image + + // Add markers if provided + var finalImage = image + if let markers = arguments["markers"] as? [[String: Any]] { + finalImage = self.addMarkers(to: image, markers: markers, snapshot: snapshot) + } + + // Convert to PNG data + guard let imageData = finalImage.pngData() else { + self.result?(FlutterError( + code: "SNAPSHOT_ERROR", + message: "Failed to convert image to PNG", + details: nil + )) + return + } + + // Return image data to Flutter + self.result?(FlutterStandardTypedData(bytes: imageData)) + } + } + + private func addMarkers(to image: UIImage, markers: [[String: Any]], snapshot: MLNMapSnapshot) -> UIImage { + print("iOS image size: \(image.size), scale: \(image.scale)") + UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale) + image.draw(at: CGPoint.zero) + + let context = UIGraphicsGetCurrentContext() + + for marker in markers { + guard let position = marker["position"] as? [String: Double], + let lat = position["latitude"], + let lng = position["longitude"], + let iconData = marker["iconData"] as? FlutterStandardTypedData, + let iconSize = marker["iconSize"] as? Double else { + continue + } + + // Convert marker coordinates to point on image + let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng) + let point = snapshot.point(for: coordinate) + + // Create icon from data + guard let iconImage = UIImage(data: iconData.data) else { continue } + print("iOS iconImage size: \(iconImage.size), scale: \(iconImage.scale)") + + // Use original icon size (already includes DPR from Flutter) + let size = iconImage.size + print("iOS marker - using original icon size: \(size.width)x\(size.height)") + let origin = CGPoint(x: point.x - size.width / 2, y: point.y - size.height) + + // Draw icon + iconImage.draw(in: CGRect(origin: origin, size: size)) + } + + let resultImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return resultImage ?? image + } +} \ No newline at end of file diff --git a/maplibre_gl/lib/src/global.dart b/maplibre_gl/lib/src/global.dart index ab10ba6f9..d2889d7dd 100644 --- a/maplibre_gl/lib/src/global.dart +++ b/maplibre_gl/lib/src/global.dart @@ -83,6 +83,74 @@ Future deleteOfflineRegion(int id) => _globalChannel.invokeMethod( }, ); +/// Creates a snapshot of a map with the specified parameters. +/// +/// [width] and [height] define the dimensions of the snapshot in pixels. +/// [styleUrl] is the URL of the map style to use. +/// [cameraPosition] defines the camera position (center and zoom level) for the snapshot. +/// [markers] is an optional list of markers to add to the snapshot. +/// +/// Returns a [Uint8List] containing the PNG image data of the snapshot. +Future startMapSnapshot({ + required int width, + required int height, + required String styleUrl, + required CameraPosition cameraPosition, + List? markers, +}) async { + final result = await _globalChannel.invokeMethod( + 'startMapSnapshot', + { + 'width': width, + 'height': height, + 'styleUrl': styleUrl, + 'cameraPosition': { + 'target': { + 'latitude': cameraPosition.target.latitude, + 'longitude': cameraPosition.target.longitude, + }, + 'zoom': cameraPosition.zoom, + 'bearing': cameraPosition.bearing, + 'tilt': cameraPosition.tilt, + }, + 'markers': markers?.map((m) => m.toMap()).toList(), + }, + ); + + if (result == null) { + throw Exception('Failed to create map snapshot'); + } + + return result; +} + +/// Represents a marker to be added to the map snapshot. +class MapMarker { + final LatLng position; + final Uint8List iconData; + final double iconSize; + final String? label; + + const MapMarker({ + required this.position, + required this.iconData, + this.iconSize = 32.0, + this.label, + }); + + Map toMap() { + return { + 'position': { + 'latitude': position.latitude, + 'longitude': position.longitude, + }, + 'iconData': iconData, + 'iconSize': iconSize, + 'label': label, + }; + } +} + Future downloadOfflineRegion( OfflineRegionDefinition definition, { Map metadata = const {}, diff --git a/maplibre_gl_example/ios/Podfile.lock b/maplibre_gl_example/ios/Podfile.lock index 816e6d081..25c456776 100644 --- a/maplibre_gl_example/ios/Podfile.lock +++ b/maplibre_gl_example/ios/Podfile.lock @@ -36,12 +36,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" SPEC CHECKSUMS: - device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - location: d5cf8598915965547c3f36761ae9cc4f4e87d22e + location: 155caecf9da4f280ab5fe4a55f94ceccfab838f8 MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd - maplibre_gl: 753f55d763a81cbdba087d02af02d12206e6f94e - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 PODFILE CHECKSUM: 7be2f5f74864d463a8ad433546ed1de7e0f29aef diff --git a/maplibre_gl_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/maplibre_gl_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15cada483..e3773d42e 100644 --- a/maplibre_gl_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/maplibre_gl_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> _allPages = [ const NoLocationPermissionPage(), const AttributionPage(), const GpsLocationPage(), + const MapSnapshotPage(), ]; class MapsDemo extends StatefulWidget { diff --git a/maplibre_gl_example/lib/map_snapshot.dart b/maplibre_gl_example/lib/map_snapshot.dart new file mode 100644 index 000000000..c307a8b9e --- /dev/null +++ b/maplibre_gl_example/lib/map_snapshot.dart @@ -0,0 +1,307 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; + +import 'page.dart'; + +class MapSnapshotPage extends ExamplePage { + const MapSnapshotPage({super.key}) + : super(const Icon(Icons.camera_alt), 'Map Snapshot'); + + @override + Widget build(BuildContext context) { + return const MapSnapshotBody(); + } +} + +class MapSnapshotBody extends StatefulWidget { + const MapSnapshotBody({super.key}); + + @override + State createState() => _MapSnapshotBodyState(); +} + +class _MapSnapshotBodyState extends State { + Uint8List? _snapshotImage; + bool _isLoading = false; + String? _error; + String? _currentCity; + double _currentZoom = 3.0; + bool _showMarkers = true; + + Future _createMarkerIcon(String label, [double pixelRatio = 1.0]) async { + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + final size = 64.0 * pixelRatio; + + // Draw emoji only with DPR consideration + final textPainter = TextPainter( + text: TextSpan( + text: label, + style: TextStyle( + fontSize: 48 * pixelRatio, + ), + ), + textDirection: TextDirection.ltr, + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset(size / 2 - textPainter.width / 2, size / 2 - textPainter.height / 2), + ); + + final picture = recorder.endRecording(); + final image = await picture.toImage(size.toInt(), size.toInt()); + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + + return byteData!.buffer.asUint8List(); + } + + Future _createFrenchFlagIcon([double pixelRatio = 1.0]) async { + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + final size = 64.0 * pixelRatio; + final flagWidth = 51.2 * pixelRatio; + final flagHeight = 38.4 * pixelRatio; + final stripeWidth = 17.066666666666666 * pixelRatio; + + // Calculate flag position (centered) + final flagX = (size - flagWidth) / 2; + final flagY = (size - flagHeight) / 2; + + // Draw French flag (blue, white, red vertical stripes) + // Blue stripe + final bluePaint = Paint()..color = const Color(0xFF0055A4); + canvas.drawRect(Rect.fromLTWH(flagX, flagY, stripeWidth, flagHeight), bluePaint); + + // White stripe + final whitePaint = Paint()..color = Colors.white; + canvas.drawRect(Rect.fromLTWH(flagX + stripeWidth, flagY, stripeWidth, flagHeight), whitePaint); + + // Red stripe + final redPaint = Paint()..color = const Color(0xFFEF4135); + canvas.drawRect(Rect.fromLTWH(flagX + stripeWidth * 2, flagY, stripeWidth, flagHeight), redPaint); + + // Draw border + final borderPaint = Paint() + ..color = Colors.black + ..style = PaintingStyle.stroke + ..strokeWidth = 1 * pixelRatio; + canvas.drawRect(Rect.fromLTWH(flagX, flagY, flagWidth, flagHeight), borderPaint); + + final picture = recorder.endRecording(); + final image = await picture.toImage(size.toInt(), size.toInt()); + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + + return byteData!.buffer.asUint8List(); + } + + Future _takeSnapshot() async { + setState(() { + _isLoading = true; + _error = null; + _snapshotImage = null; + _currentCity = null; + }); + + try { + // Define random camera positions for different cities + final cities = [ + {'name': 'Beijing', 'lat': 39.9042, 'lng': 116.4074}, + {'name': 'San Francisco', 'lat': 37.7749, 'lng': -122.4194}, + {'name': 'London', 'lat': 51.5074, 'lng': -0.1278}, + {'name': 'Sydney', 'lat': -33.8688, 'lng': 151.2093}, + ]; + + // Pick a random city + final randomCity = cities[DateTime.now().millisecond % cities.length]; + + final cameraPosition = CameraPosition( + target: LatLng(randomCity['lat'] as double, randomCity['lng'] as double), + zoom: _currentZoom, + ); + + // Take the snapshot with DPR consideration + final pixelRatio = MediaQuery.of(context).devicePixelRatio; + + // Create markers if enabled + List? markers; + if (_showMarkers) { + final cityName = randomCity['name'] as String; + + markers = [ + MapMarker( + position: LatLng(randomCity['lat'] as double, randomCity['lng'] as double), + iconData: await _createMarkerIcon('📍', pixelRatio), + iconSize: 36.0, + label: cityName, + ), + MapMarker( + position: LatLng(48.8566, 2.3522), + iconData: await _createFrenchFlagIcon(pixelRatio), + iconSize: 24.0, + label: 'Paris', + ), + ]; + } + final imageData = await startMapSnapshot( + width: (400 * pixelRatio).toInt(), + height: (300 * pixelRatio).toInt(), + styleUrl: 'https://demotiles.maplibre.org/style.json', + cameraPosition: cameraPosition, + markers: markers, + ); + + setState(() { + _snapshotImage = imageData; + _currentCity = randomCity['name'] as String; + _isLoading = false; + }); + } catch (e) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + ElevatedButton( + onPressed: _isLoading ? null : _takeSnapshot, + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Take Map Snapshot'), + ), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Zoom Level: ${_currentZoom.toStringAsFixed(1)}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Slider( + value: _currentZoom, + min: 1.0, + max: 20.0, + divisions: 19, + onChanged: (value) { + setState(() { + _currentZoom = value; + }); + }, + ), + const Text( + 'Zoom: 1.0 (World) - 20.0 (Streets)', + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + const SizedBox(height: 8), + Row( + children: [ + const Text('Show Markers'), + const SizedBox(width: 8), + Switch( + value: _showMarkers, + onChanged: (value) { + setState(() { + _showMarkers = value; + }); + }, + ), + ], + ), + ], + ), + ], + ), + ), + Expanded( + child: Center( + child: _buildContent(), + ), + ), + ], + ); + } + + Widget _buildContent() { + if (_isLoading) { + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Generating snapshot...'), + ], + ); + } + + if (_error != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error, color: Colors.red, size: 48), + const SizedBox(height: 16), + Text( + 'Error: $_error', + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), + ], + ); + } + + if (_snapshotImage != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Map Snapshot - $_currentCity', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Image.memory( + _snapshotImage!, + width: 400, + height: 300, + fit: BoxFit.contain, + ), + const SizedBox(height: 16), + Text( + 'Snapshot of $_currentCity with zoom level ${_currentZoom.toStringAsFixed(1)}', + style: const TextStyle(color: Colors.grey), + ), + ], + ); + } + + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.camera_alt, size: 48, color: Colors.grey), + SizedBox(height: 16), + Text( + 'Press the button above to take a map snapshot', + style: TextStyle(color: Colors.grey), + ), + ], + ); + } +} diff --git a/maplibre_gl_example/pubspec.yaml b/maplibre_gl_example/pubspec.yaml index cce40356f..3f5cdd1ac 100644 --- a/maplibre_gl_example/pubspec.yaml +++ b/maplibre_gl_example/pubspec.yaml @@ -17,7 +17,8 @@ dependencies: flutter_hooks: ^0.20.5 http: ^1.1.0 location: ^5.0.3 - maplibre_gl: ^0.22.0 + maplibre_gl: + path: ../maplibre_gl path_provider: ^2.1.5 dev_dependencies: