From 8ef189bb1053e20932b8b90310f77eb3a323a714 Mon Sep 17 00:00:00 2001 From: SotaTamura Date: Tue, 3 Mar 2026 16:47:28 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E3=83=94=E3=83=B3=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/map/presentation/map_screen.dart | 300 +++++++++++++----- 1 file changed, 215 insertions(+), 85 deletions(-) diff --git a/lib/features/map/presentation/map_screen.dart b/lib/features/map/presentation/map_screen.dart index f606fbd..df4762d 100644 --- a/lib/features/map/presentation/map_screen.dart +++ b/lib/features/map/presentation/map_screen.dart @@ -19,6 +19,8 @@ class MapScreen extends ConsumerStatefulWidget { class _MapScreenState extends ConsumerState { late final MapController _mapController; + bool _isDimmed = false; // ピン選択時に背景を暗くする + PinData? _activePin; // 選択されたピン @override void initState() { @@ -34,12 +36,26 @@ class _MapScreenState extends ConsumerState { List _buildMarkers(List pins) { return pins.map((pin) { + final isActive = _activePin?.id == pin.id; return Marker( point: pin.position, width: 60, height: 60, alignment: Alignment.topCenter, - child: const Icon(Icons.location_pin, size: 60, color: Colors.red), + child: Opacity( + opacity: isActive ? 0.0 : 1.0, + child: _AnimatedMarker( + key: ValueKey(pin.id), + pin: pin, + ref: ref, + onFloatingChanged: (isFloating) { + setState(() { + _isDimmed = isFloating; + _activePin = isFloating ? pin : null; + }); + }, + ), + ), ); }).toList(); } @@ -74,103 +90,217 @@ class _MapScreenState extends ConsumerState { ), ], ), - body: Column( + body: Stack( children: [ - Expanded( - child: Stack( - children: [ - FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: const LatLng(35.6895, 139.6917), - initialZoom: 9.2, - interactionOptions: InteractionOptions( - flags: drawingState.isDrawingMode - ? InteractiveFlag.none - : InteractiveFlag.all & - ~InteractiveFlag.doubleTapZoom, - ), - onTap: (tapPosition, latlng) { - if (!drawingState.isDrawingMode) { - ref.read(pinsProvider.notifier).addPin(latlng); - } - }, - ), + Column( + children: [ + Expanded( + child: Stack( children: [ - TileLayer( - urlTemplate: - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), - PolylineLayer( - polylines: drawingState.paths - .map( - (path) => Polyline( - points: path.points, - color: path.color, - strokeWidth: path.strokeWidth, - ), - ) - .toList(), - ), - MarkerLayer( - markers: pinsAsync.when( - data: _buildMarkers, - loading: () => [], - error: (_, _) => [], + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: const LatLng(35.6895, 139.6917), + initialZoom: 9.2, + interactionOptions: InteractionOptions( + flags: drawingState.isDrawingMode + ? InteractiveFlag.none + : InteractiveFlag.all & + ~InteractiveFlag.doubleTapZoom, + ), + onTap: (tapPosition, latlng) { + if (!drawingState.isDrawingMode) { + ref.read(pinsProvider.notifier).addPin(latlng); + } + }, ), - ), - RichAttributionWidget( - alignment: AttributionAlignment.bottomLeft, - attributions: [ - TextSourceAttribution( - 'OpenStreetMap contributors', - onTap: () => launchUrl( - Uri.parse('https://openstreetmap.org/copyright'), + children: [ + TileLayer( + urlTemplate: + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: + 'dev.fleaflet.flutter_map.example', + ), + PolylineLayer( + polylines: drawingState.paths + .map( + (path) => Polyline( + points: path.points, + color: path.color, + strokeWidth: path.strokeWidth, + ), + ) + .toList(), + ), + MarkerLayer( + markers: pinsAsync.when( + data: _buildMarkers, + loading: () => [], + error: (_, _) => [], ), ), + RichAttributionWidget( + alignment: AttributionAlignment.bottomLeft, + attributions: [ + TextSourceAttribution( + 'OpenStreetMap contributors', + onTap: () => launchUrl( + Uri.parse( + 'https://openstreetmap.org/copyright', + ), + ), + ), + ], + ), ], ), - ], - ), - IgnorePointer( - ignoring: !drawingState.isDrawingMode, - child: DrawingCanvas(mapController: _mapController), - ), - Positioned( - right: 16, - bottom: 16, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FloatingActionButton( - heroTag: 'zoom_in', - onPressed: () => _mapController.move( - _mapController.camera.center, - _mapController.camera.zoom + 1, - ), - tooltip: 'Zoom in', - child: const Icon(Icons.add), - ), - const SizedBox(height: 8), - FloatingActionButton( - heroTag: 'zoom_out', - onPressed: () => _mapController.move( - _mapController.camera.center, - _mapController.camera.zoom - 1, - ), - tooltip: 'Zoom out', - child: const Icon(Icons.remove), + IgnorePointer( + ignoring: !drawingState.isDrawingMode, + child: DrawingCanvas(mapController: _mapController), + ), + Positioned( + right: 16, + bottom: 16, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FloatingActionButton( + heroTag: 'zoom_in', + onPressed: () => _mapController.move( + _mapController.camera.center, + _mapController.camera.zoom + 1, + ), + tooltip: 'Zoom in', + child: const Icon(Icons.add), + ), + const SizedBox(height: 8), + FloatingActionButton( + heroTag: 'zoom_out', + onPressed: () => _mapController.move( + _mapController.camera.center, + _mapController.camera.zoom - 1, + ), + tooltip: 'Zoom out', + child: const Icon(Icons.remove), + ), + ], ), - ], - ), + ), + ], ), - ], + ), + const Controls(), + ], + ), + // ピン選択時に背景を暗くするウィジェット + IgnorePointer( + ignoring: !_isDimmed, + child: AnimatedOpacity( + opacity: _isDimmed ? 1.0 : 0.0, + duration: const Duration(milliseconds: 200), + child: Container(color: Colors.black.withValues(alpha: 0.4)), ), ), - const Controls(), + // ピン選択時に浮き上がるピン + if (_isDimmed && _activePin != null) + Builder( + builder: (context) { + final pos = _mapController.camera.latLngToScreenOffset( + _activePin!.position, + ); + return Positioned( + left: pos.dx - 30, + top: pos.dy - 60, + width: 60, + height: 60, + child: const _PinIcon(isFloating: true), + ); + }, + ), ], ), ); } } + +class _AnimatedMarker extends StatefulWidget { + final PinData pin; + final WidgetRef ref; + final void Function(bool) onFloatingChanged; + + const _AnimatedMarker({ + super.key, + required this.pin, + required this.ref, + required this.onFloatingChanged, + }); + + @override + State<_AnimatedMarker> createState() => _AnimatedMarkerState(); +} + +class _AnimatedMarkerState extends State<_AnimatedMarker> { + bool _isFloating = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onLongPressStart: (details) async { + setState(() { + _isFloating = true; + }); + widget.onFloatingChanged(true); + + final selected = await showMenu( + context: context, + position: RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + details.globalPosition.dy, + ), + items: const [ + PopupMenuItem( + value: "delete", + child: Text("Delete", style: TextStyle(color: Colors.red)), + ), + ], + ); + + if (mounted) { + setState(() { + _isFloating = false; + }); + widget.onFloatingChanged(false); + } + + if (selected == "delete") { + widget.ref.read(pinsProvider.notifier).deletePin(widget.pin.id); + } + }, + child: _PinIcon(isFloating: _isFloating), + ); + } +} + +class _PinIcon extends StatelessWidget { + final bool isFloating; + + const _PinIcon({required this.isFloating}); + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + alignment: Alignment.bottomCenter, + transform: Matrix4.diagonal3Values( + isFloating ? 1.2 : 1.0, + isFloating ? 1.2 : 1.0, + 1.0, + ), + transformAlignment: Alignment.bottomCenter, + child: Icon(Icons.location_on, color: Colors.red, size: 40), + ); + } +} From 9b65dca47a56ac85e00f2bee22caa0c1af051e13 Mon Sep 17 00:00:00 2001 From: SotaTamura Date: Fri, 13 Mar 2026 23:16:53 +0900 Subject: [PATCH 2/2] =?UTF-8?q?mapbox=E7=A7=BB=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- assets/pin.png | Bin 0 -> 5204 bytes ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 2 +- ios/Podfile.lock | 27 +- ios/Runner.xcodeproj/project.pbxproj | 6 +- lib/features/map/presentation/map_screen.dart | 629 +++++++++++++----- .../presentation/widgets/drawing_canvas.dart | 225 ------- lib/main.dart | 4 + pubspec.lock | 80 +++ pubspec.yaml | 2 + 11 files changed, 578 insertions(+), 403 deletions(-) create mode 100644 assets/pin.png diff --git a/.gitignore b/.gitignore index 0cbf081..c858208 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,6 @@ app.*.map.json .env .dev.vars node_modules -.wrangler \ No newline at end of file +.wrangler + +.vscode/ \ No newline at end of file diff --git a/assets/pin.png b/assets/pin.png new file mode 100644 index 0000000000000000000000000000000000000000..2792ace607e30c6b1f333bfd6d9ac8aaf30dc7a4 GIT binary patch literal 5204 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 VE{9bS%U*T{7#}x3_Q-%5`F~5~^uGWA literal 0 HcmV?d00001 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7..163000d 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 13.0 + 14.0 diff --git a/ios/Podfile b/ios/Podfile index 3c51025..fa57b45 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,7 +1,7 @@ source 'https://github.com/CocoaPods/Specs.git' # Uncomment this line to define a global platform for your project -platform :ios, '13.0' +platform :ios, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a0bde07..5515e42 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -40,10 +40,23 @@ PODS: - GTMSessionFetcher/Core (3.5.0) - GTMSessionFetcher/Full (3.5.0): - GTMSessionFetcher/Core + - mapbox_maps_flutter (2.19.1): + - Flutter + - MapboxMaps (= 11.19.0) + - Turf (= 4.0.0) + - MapboxCommon (24.19.0): + - Turf (= 4.0.0) + - MapboxCoreMaps (11.19.0): + - MapboxCommon (= 24.19.0) + - MapboxMaps (11.19.0): + - MapboxCommon (= 24.19.0) + - MapboxCoreMaps (= 11.19.0) + - Turf (= 4.0.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - PromisesObjC (2.4.0) + - Turf (4.0.0) - url_launcher_ios (0.0.1): - Flutter @@ -51,6 +64,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) + - mapbox_maps_flutter (from `.symlinks/plugins/mapbox_maps_flutter/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -62,7 +76,11 @@ SPEC REPOS: - GoogleUtilities - GTMAppAuth - GTMSessionFetcher + - MapboxCommon + - MapboxCoreMaps + - MapboxMaps - PromisesObjC + - Turf EXTERNAL SOURCES: Flutter: @@ -71,6 +89,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage/ios" google_sign_in_ios: :path: ".symlinks/plugins/google_sign_in_ios/darwin" + mapbox_maps_flutter: + :path: ".symlinks/plugins/mapbox_maps_flutter/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" url_launcher_ios: @@ -86,10 +106,15 @@ SPEC CHECKSUMS: GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + mapbox_maps_flutter: 632563764d2dff820a123312fa5b50417a214b1f + MapboxCommon: b40bcf330493e0d218802a63cabbd2efe6ca303b + MapboxCoreMaps: badf69766576702ccb09a901c5859a45f0f600c0 + MapboxMaps: db05259e1ea68e9a8a4e6f9014027418025f2ebe path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + Turf: c9eb11a65d96af58cac523460fd40fec5061b081 url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b -PODFILE CHECKSUM: 9ca1682d392c3b88ad6d313259873375a305cdca +PODFILE CHECKSUM: 9d0963e5127f1378f44f0c102f08e544ad83bf96 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4dadb88..d715090 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -473,7 +473,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -602,7 +602,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -653,7 +653,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/features/map/presentation/map_screen.dart b/lib/features/map/presentation/map_screen.dart index df4762d..1d659dd 100644 --- a/lib/features/map/presentation/map_screen.dart +++ b/lib/features/map/presentation/map_screen.dart @@ -1,14 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:latlong2/latlong.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:memomap/features/auth/providers/auth_provider.dart'; import 'package:memomap/features/map/providers/pin_provider.dart'; import 'package:memomap/features/map/providers/drawing_provider.dart'; -import 'package:memomap/features/map/presentation/widgets/drawing_canvas.dart'; +import 'package:memomap/features/map/models/drawing_path.dart'; import 'package:memomap/features/map/presentation/widgets/controls.dart'; +import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; +import 'package:flutter/services.dart'; +import 'dart:math' as math; +import 'dart:convert'; class MapScreen extends ConsumerStatefulWidget { const MapScreen({super.key}); @@ -18,55 +20,409 @@ class MapScreen extends ConsumerStatefulWidget { } class _MapScreenState extends ConsumerState { - late final MapController _mapController; + MapboxMap? _mapboxMap; + PointAnnotationManager? pointAnnotationManager; bool _isDimmed = false; // ピン選択時に背景を暗くする PinData? _activePin; // 選択されたピン + Offset? _activePinScreenPos; // 選択されたピンのスクリーン座標 + Uint8List? _pinImageData; + List _currentLatLngs = []; + Offset? _eraserPosition; + final Map _annotationToPin = {}; @override void initState() { super.initState(); - _mapController = MapController(); + _loadPinImage(); } - @override - void dispose() { - _mapController.dispose(); - super.dispose(); + Future _loadPinImage() async { + final ByteData bytes = await rootBundle.load('assets/pin.png'); + _pinImageData = bytes.buffer.asUint8List(); + } + + void _onMapCreated(MapboxMap mapboxMap) async { + _mapboxMap = mapboxMap; + pointAnnotationManager = await mapboxMap.annotations + .createPointAnnotationManager(); + + pointAnnotationManager?.longPressEvents( + onLongPress: (annotation) { + _handlePinLongPress(annotation); + }, + ); + + _updatePins(); + + setState(() {}); // _mapboxMapが設定されたことを通知 } - List _buildMarkers(List pins) { - return pins.map((pin) { - final isActive = _activePin?.id == pin.id; - return Marker( - point: pin.position, - width: 60, - height: 60, - alignment: Alignment.topCenter, - child: Opacity( - opacity: isActive ? 0.0 : 1.0, - child: _AnimatedMarker( - key: ValueKey(pin.id), - pin: pin, - ref: ref, - onFloatingChanged: (isFloating) { - setState(() { - _isDimmed = isFloating; - _activePin = isFloating ? pin : null; - }); - }, + Future _updatePins() async { + if (pointAnnotationManager == null || _pinImageData == null) return; + await pointAnnotationManager!.deleteAll(); + _annotationToPin.clear(); + + final pins = ref.read(pinsProvider).value ?? []; + for (PinData pin in pins) { + final annotation = await pointAnnotationManager!.create( + PointAnnotationOptions( + geometry: Point( + coordinates: Position( + pin.position.longitude, + pin.position.latitude, + ), ), + image: _pinImageData, + iconSize: 0.5, + iconAnchor: IconAnchor.BOTTOM, + ), + ); + _annotationToPin[annotation.id] = pin; + } + } + + void _handlePinLongPress(PointAnnotation annotation) async { + final pin = _annotationToPin[annotation.id]; + if (pin == null || _mapboxMap == null) return; + + final screenPos = await _mapboxMap!.pixelForCoordinate( + Point( + coordinates: Position(pin.position.longitude, pin.position.latitude), + ), + ); + + setState(() { + _isDimmed = true; + _activePin = pin; + _activePinScreenPos = Offset( + screenPos.x.toDouble(), + screenPos.y.toDouble(), + ); + }); + + if (!mounted) return; + + final selected = await showMenu( + context: context, + position: RelativeRect.fromLTRB( + _activePinScreenPos!.dx, + _activePinScreenPos!.dy, + _activePinScreenPos!.dx, + _activePinScreenPos!.dy, + ), + items: const [ + PopupMenuItem( + value: "delete", + child: Text("Delete", style: TextStyle(color: Colors.red)), + ), + ], + ); + + if (mounted) { + setState(() { + _isDimmed = false; + _activePin = null; + _activePinScreenPos = null; + }); + } + + if (selected == "delete") { + ref.read(pinsProvider.notifier).deletePin(pin.id); + } + } + + void _onStyleLoaded(StyleLoadedEventData data) async { + final style = _mapboxMap?.style; + if (style == null) return; + + // 既存のパス用ソースとレイヤー + await style.addSource(GeoJsonSource(id: "existing_paths_source")); + await style.addLayer( + LineLayer( + id: "existing_paths_layer", + sourceId: "existing_paths_source", + lineJoin: LineJoin.ROUND, + lineCap: LineCap.ROUND, + ), + ); + // データ駆動型スタイリングの設定 + await style.setStyleLayerProperty("existing_paths_layer", "line-color", [ + "get", + "color", + ]); + await style.setStyleLayerProperty("existing_paths_layer", "line-width", [ + "get", + "width", + ]); + + // 現在描画中のパス用ソースとレイヤー + await style.addSource(GeoJsonSource(id: "current_path_source")); + await style.addLayer( + LineLayer( + id: "current_path_layer", + sourceId: "current_path_source", + lineJoin: LineJoin.ROUND, + lineCap: LineCap.ROUND, + ), + ); + + _updateLines(); + } + + String _colorToRgb(Color color) { + return 'rgb(${(color.r * 255.0).round().clamp(0, 255)}, ${(color.g * 255.0).round().clamp(0, 255)}, ${(color.b * 255.0).round().clamp(0, 255)})'; + } + + Future _updateLines() async { + final style = _mapboxMap?.style; + if (style == null) return; + + final drawingState = ref.read(drawingProvider); + final features = drawingState.paths.asMap().entries.map((entry) { + final index = entry.key; + final path = entry.value; + return Feature( + id: "path_$index", + geometry: LineString( + coordinates: path.points + .map((p) => Position(p.longitude, p.latitude)) + .toList(), ), + properties: { + "color": _colorToRgb(path.color), + "width": path.strokeWidth, + }, ); }).toList(); + + try { + final collection = FeatureCollection(features: features); + await style.setStyleSourceProperty( + "existing_paths_source", + "data", + jsonEncode(collection.toJson()), + ); + } catch (e) { + debugPrint("Error updating existing_paths_source: $e"); + } + } + + Future _updateCurrentPath() async { + final style = _mapboxMap?.style; + if (style == null) return; + + final drawingState = ref.read(drawingProvider); + final features = _currentLatLngs.isEmpty + ? [] + : [ + Feature( + id: "current_path", + geometry: LineString( + coordinates: _currentLatLngs + .map((p) => Position(p.longitude, p.latitude)) + .toList(), + ), + ), + ]; + + try { + final collection = FeatureCollection(features: features); + await style.setStyleSourceProperty( + "current_path_source", + "data", + jsonEncode(collection.toJson()), + ); + + if (_currentLatLngs.isNotEmpty) { + await style.setStyleLayerProperty( + "current_path_layer", + "line-color", + _colorToRgb(drawingState.selectedColor), + ); + await style.setStyleLayerProperty( + "current_path_layer", + "line-width", + drawingState.strokeWidth, + ); + } + } catch (e) { + debugPrint("Error updating current_path_source: $e"); + } + } + + void _onMapTap(MapContentGestureContext context) { + if (ref.read(drawingProvider).isDrawingMode) return; + final latLng = LatLng( + context.point.coordinates.lat.toDouble(), + context.point.coordinates.lng.toDouble(), + ); + ref.read(pinsProvider.notifier).addPin(latLng); + } + + Future _convertToLatLng(Offset offset) async { + if (_mapboxMap == null) return; + final point = await _mapboxMap!.coordinateForPixel( + ScreenCoordinate(x: offset.dx, y: offset.dy), + ); + if (mounted) { + setState(() { + _currentLatLngs.add( + LatLng( + point.coordinates.lat.toDouble(), + point.coordinates.lng.toDouble(), + ), + ); + }); + } + } + + Future _handleEraser(Offset localPosition) async { + if (_mapboxMap == null) return; + final drawingState = ref.read(drawingProvider); + final drawingNotifier = ref.read(drawingProvider.notifier); + + final point = await _mapboxMap!.coordinateForPixel( + ScreenCoordinate(x: localPosition.dx, y: localPosition.dy), + ); + final latLng = LatLng( + point.coordinates.lat.toDouble(), + point.coordinates.lng.toDouble(), + ); + + final cameraState = await _mapboxMap!.getCameraState(); + final zoom = cameraState.zoom; + + final distance = const Distance(); + + // 消しゴムの半径(メートル換算)。 + // Mapboxは512pxタイルを使用するため、ズーム0での1ピクセルあたりのメートル数は約78271.5 + final metersPerPixel = + 78271.51696 * + math.cos(latLng.latitude * math.pi / 180) / + math.pow(2, zoom); + final eraserRadius = (drawingState.strokeWidth * 2) * metersPerPixel; + + List newPaths = []; + bool changed = false; + + for (final path in drawingState.paths) { + List currentSegment = []; + bool pathModified = false; + + for (final p in path.points) { + if (distance(latLng, p) < eraserRadius) { + if (currentSegment.length > 1) { + newPaths.add( + DrawingPath( + points: List.from(currentSegment), + color: path.color, + strokeWidth: path.strokeWidth, + ), + ); + } + currentSegment = []; + pathModified = true; + changed = true; + } else { + currentSegment.add(p); + } + } + + if (currentSegment.length > 1) { + newPaths.add( + DrawingPath( + points: currentSegment, + color: path.color, + strokeWidth: path.strokeWidth, + ), + ); + } else if (pathModified && currentSegment.length <= 1) { + } else if (!pathModified) { + newPaths.add(path); + } + } + + if (changed) { + drawingNotifier.setPaths(newPaths); + } + } + + void _onPanStart(DragStartDetails details) async { + final drawingState = ref.read(drawingProvider); + if (drawingState.isEraserMode) { + setState(() { + _eraserPosition = details.localPosition; + }); + await _handleEraser(details.localPosition); + return; + } + + _currentLatLngs = []; + await _convertToLatLng(details.localPosition); + await _updateCurrentPath(); + } + + void _onPanUpdate(DragUpdateDetails details) async { + final drawingState = ref.read(drawingProvider); + if (drawingState.isEraserMode) { + setState(() { + _eraserPosition = details.localPosition; + }); + await _handleEraser(details.localPosition); + return; + } + + await _convertToLatLng(details.localPosition); + await _updateCurrentPath(); + } + + void _onPanEnd(DragEndDetails details) async { + final drawingState = ref.read(drawingProvider); + if (drawingState.isEraserMode) { + setState(() { + _eraserPosition = null; + }); + return; + } + + if (_currentLatLngs.length > 1) { + ref + .read(drawingProvider.notifier) + .addPath( + DrawingPath( + points: List.from(_currentLatLngs), + color: drawingState.selectedColor, + strokeWidth: drawingState.strokeWidth, + ), + ); + } + + _currentLatLngs = []; + await _updateCurrentPath(); + setState(() {}); + } + + @override + void dispose() { + super.dispose(); } @override Widget build(BuildContext context) { final isAuthenticated = ref.watch(isAuthenticatedProvider); final user = ref.watch(currentUserProvider); - final pinsAsync = ref.watch(pinsProvider); final drawingState = ref.watch(drawingProvider); + ref.listen(drawingProvider.select((s) => s.paths), (previous, next) { + _updateLines(); + }); + + ref.listen(pinsProvider, (previous, next) { + _updatePins(); + }); + + MapboxMapsOptions.setLanguage("ja"); + return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, @@ -97,89 +453,87 @@ class _MapScreenState extends ConsumerState { Expanded( child: Stack( children: [ - FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: const LatLng(35.6895, 139.6917), - initialZoom: 9.2, - interactionOptions: InteractionOptions( - flags: drawingState.isDrawingMode - ? InteractiveFlag.none - : InteractiveFlag.all & - ~InteractiveFlag.doubleTapZoom, - ), - onTap: (tapPosition, latlng) { - if (!drawingState.isDrawingMode) { - ref.read(pinsProvider.notifier).addPin(latlng); - } - }, + MapWidget( + cameraOptions: CameraOptions( + center: Point(coordinates: Position(139.767, 35.681)), + zoom: 12, + bearing: 0, + pitch: 0, ), - children: [ - TileLayer( - urlTemplate: - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: - 'dev.fleaflet.flutter_map.example', - ), - PolylineLayer( - polylines: drawingState.paths - .map( - (path) => Polyline( - points: path.points, - color: path.color, - strokeWidth: path.strokeWidth, + onMapCreated: _onMapCreated, + onStyleLoadedListener: _onStyleLoaded, + onTapListener: _onMapTap, + gestureRecognizers: drawingState.isDrawingMode + ? {} + : null, + ), + if (_mapboxMap != null) + IgnorePointer( + ignoring: !drawingState.isDrawingMode, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanStart: _onPanStart, + onPanUpdate: _onPanUpdate, + onPanEnd: _onPanEnd, + child: Stack( + children: [ + Container(color: Colors.transparent), + if (_eraserPosition != null) + Positioned( + left: + _eraserPosition!.dx - + drawingState.strokeWidth * 2, + top: + _eraserPosition!.dy - + drawingState.strokeWidth * 2, + child: Container( + width: drawingState.strokeWidth * 4, + height: drawingState.strokeWidth * 4, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.black.withValues( + alpha: 0.5, + ), + width: 1.0, + ), + ), + ), ), - ) - .toList(), - ), - MarkerLayer( - markers: pinsAsync.when( - data: _buildMarkers, - loading: () => [], - error: (_, _) => [], + ], ), ), - RichAttributionWidget( - alignment: AttributionAlignment.bottomLeft, - attributions: [ - TextSourceAttribution( - 'OpenStreetMap contributors', - onTap: () => launchUrl( - Uri.parse( - 'https://openstreetmap.org/copyright', - ), - ), - ), - ], - ), - ], - ), - IgnorePointer( - ignoring: !drawingState.isDrawingMode, - child: DrawingCanvas(mapController: _mapController), - ), + ), Positioned( right: 16, - bottom: 16, + bottom: 40, child: Column( mainAxisSize: MainAxisSize.min, children: [ FloatingActionButton( heroTag: 'zoom_in', - onPressed: () => _mapController.move( - _mapController.camera.center, - _mapController.camera.zoom + 1, - ), + onPressed: () async { + final camera = await _mapboxMap?.getCameraState(); + if (camera != null) { + _mapboxMap?.setCamera( + CameraOptions(zoom: camera.zoom + 1), + ); + } + }, tooltip: 'Zoom in', child: const Icon(Icons.add), ), const SizedBox(height: 8), FloatingActionButton( heroTag: 'zoom_out', - onPressed: () => _mapController.move( - _mapController.camera.center, - _mapController.camera.zoom - 1, - ), + onPressed: () async { + final camera = await _mapboxMap?.getCameraState(); + if (camera != null) { + _mapboxMap?.setCamera( + CameraOptions(zoom: camera.zoom - 1), + ); + } + }, tooltip: 'Zoom out', child: const Icon(Icons.remove), ), @@ -202,20 +556,13 @@ class _MapScreenState extends ConsumerState { ), ), // ピン選択時に浮き上がるピン - if (_isDimmed && _activePin != null) - Builder( - builder: (context) { - final pos = _mapController.camera.latLngToScreenOffset( - _activePin!.position, - ); - return Positioned( - left: pos.dx - 30, - top: pos.dy - 60, - width: 60, - height: 60, - child: const _PinIcon(isFloating: true), - ); - }, + if (_isDimmed && _activePin != null && _activePinScreenPos != null) + Positioned( + left: _activePinScreenPos!.dx - 18, + top: _activePinScreenPos!.dy - 36, + width: 36, + height: 36, + child: const _PinIcon(isFloating: true), ), ], ), @@ -223,66 +570,6 @@ class _MapScreenState extends ConsumerState { } } -class _AnimatedMarker extends StatefulWidget { - final PinData pin; - final WidgetRef ref; - final void Function(bool) onFloatingChanged; - - const _AnimatedMarker({ - super.key, - required this.pin, - required this.ref, - required this.onFloatingChanged, - }); - - @override - State<_AnimatedMarker> createState() => _AnimatedMarkerState(); -} - -class _AnimatedMarkerState extends State<_AnimatedMarker> { - bool _isFloating = false; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onLongPressStart: (details) async { - setState(() { - _isFloating = true; - }); - widget.onFloatingChanged(true); - - final selected = await showMenu( - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - details.globalPosition.dy, - ), - items: const [ - PopupMenuItem( - value: "delete", - child: Text("Delete", style: TextStyle(color: Colors.red)), - ), - ], - ); - - if (mounted) { - setState(() { - _isFloating = false; - }); - widget.onFloatingChanged(false); - } - - if (selected == "delete") { - widget.ref.read(pinsProvider.notifier).deletePin(widget.pin.id); - } - }, - child: _PinIcon(isFloating: _isFloating), - ); - } -} - class _PinIcon extends StatelessWidget { final bool isFloating; @@ -300,7 +587,7 @@ class _PinIcon extends StatelessWidget { 1.0, ), transformAlignment: Alignment.bottomCenter, - child: Icon(Icons.location_on, color: Colors.red, size: 40), + child: Image.asset('assets/pin.png', fit: BoxFit.contain), ); } } diff --git a/lib/features/map/presentation/widgets/drawing_canvas.dart b/lib/features/map/presentation/widgets/drawing_canvas.dart index 6b10b20..8b13789 100644 --- a/lib/features/map/presentation/widgets/drawing_canvas.dart +++ b/lib/features/map/presentation/widgets/drawing_canvas.dart @@ -1,226 +1 @@ -import 'dart:math' as math; -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:latlong2/latlong.dart' hide Path; -import 'package:memomap/features/map/models/drawing_path.dart'; -import 'package:memomap/features/map/providers/drawing_provider.dart'; -class DrawingCanvas extends ConsumerStatefulWidget { - final MapController mapController; - const DrawingCanvas({super.key, required this.mapController}); - - @override - ConsumerState createState() => _DrawingCanvasState(); -} - -class _DrawingCanvasState extends ConsumerState { - DrawingPath? _currentPath; - Offset? _eraserPosition; - - void _handleEraser(Offset localPosition) { - final drawingState = ref.read(drawingProvider); - final drawingNotifier = ref.read(drawingProvider.notifier); - - final latLng = widget.mapController.camera.screenOffsetToLatLng( - localPosition, - ); - final distance = const Distance(); - - // 消しゴムの半径(メートル換算)。strokeWidthを基準にする - final metersPerPixel = - 156543.03392 * - math.cos(latLng.latitude * math.pi / 180) / - math.pow(2, widget.mapController.camera.zoom); - final eraserRadius = drawingState.strokeWidth * metersPerPixel * 2; - - List newPaths = []; - bool changed = false; - - for (final path in drawingState.paths) { - List currentSegment = []; - bool pathModified = false; - - for (final point in path.points) { - if (distance(latLng, point) < eraserRadius) { - if (currentSegment.length > 1) { - newPaths.add( - DrawingPath( - points: List.from(currentSegment), - color: path.color, - strokeWidth: path.strokeWidth, - ), - ); - } - currentSegment = []; - pathModified = true; - changed = true; - } else { - currentSegment.add(point); - } - } - - // 最後のセグメントを追加 - if (currentSegment.length > 1) { - newPaths.add( - DrawingPath( - points: currentSegment, - color: path.color, - strokeWidth: path.strokeWidth, - ), - ); - } else if (pathModified && currentSegment.length <= 1) { - // セグメントが短くなりすぎた場合は追加しない - } else if (!pathModified) { - // 修正がなかった場合は元のパスを維持 - newPaths.add(path); - } - } - - if (changed) { - drawingNotifier.setPaths(newPaths); - } - } - - @override - Widget build(BuildContext context) { - final drawingState = ref.watch(drawingProvider); - final drawingNotifier = ref.read(drawingProvider.notifier); - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onPanStart: (details) { - if (!drawingState.isDrawingMode) return; - - if (drawingState.isEraserMode) { - setState(() { - _eraserPosition = details.localPosition; - }); - _handleEraser(details.localPosition); - return; - } - - final latLng = widget.mapController.camera.screenOffsetToLatLng( - details.localPosition, - ); - setState(() { - _currentPath = DrawingPath( - points: [latLng], - color: drawingState.selectedColor, - strokeWidth: drawingState.strokeWidth, - ); - }); - }, - onPanUpdate: (details) { - if (!drawingState.isDrawingMode) return; - - if (drawingState.isEraserMode) { - setState(() { - _eraserPosition = details.localPosition; - }); - _handleEraser(details.localPosition); - return; - } - - if (_currentPath == null) return; - final latLng = widget.mapController.camera.screenOffsetToLatLng( - details.localPosition, - ); - setState(() { - _currentPath = _currentPath!.copyWith( - points: [..._currentPath!.points, latLng], - ); - }); - }, - onPanEnd: (details) { - if (drawingState.isEraserMode) { - setState(() { - _eraserPosition = null; - }); - return; - } - - if (_currentPath != null && _currentPath!.points.length > 1) { - drawingNotifier.addPath(_currentPath!); - } - setState(() { - _currentPath = null; - }); - }, - child: Stack( - children: [ - if (_currentPath != null) - CustomPaint( - size: Size.infinite, - painter: _CurrentPathPainter(_currentPath!, widget.mapController), - ), - if (_eraserPosition != null) - CustomPaint( - size: Size.infinite, - painter: _EraserPainter( - _eraserPosition!, - drawingState.strokeWidth * 2, - ), - ), - ], - ), - ); - } -} - -class _CurrentPathPainter extends CustomPainter { - final DrawingPath drawingPath; - final MapController mapController; - - _CurrentPathPainter(this.drawingPath, this.mapController); - - @override - void paint(Canvas canvas, Size size) { - if (drawingPath.points.length < 2) return; - - final paint = Paint() - ..strokeCap = StrokeCap.round - ..strokeJoin = StrokeJoin.round - ..isAntiAlias = true - ..color = drawingPath.color - ..strokeWidth = drawingPath.strokeWidth - ..style = PaintingStyle.stroke; - - final path = Path(); - for (var i = 0; i < drawingPath.points.length; i++) { - final offset = mapController.camera.latLngToScreenOffset( - drawingPath.points[i], - ); - if (i == 0) { - path.moveTo(offset.dx, offset.dy); - } else { - path.lineTo(offset.dx, offset.dy); - } - } - - canvas.drawPath(path, paint); - } - - @override - bool shouldRepaint(_CurrentPathPainter oldDelegate) => true; -} - -class _EraserPainter extends CustomPainter { - final Offset position; - final double radius; - - _EraserPainter(this.position, this.radius); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = Colors.black.withValues(alpha: 0.5) - ..style = PaintingStyle.stroke - ..strokeWidth = 1.0; - canvas.drawCircle(position, radius, paint); - } - - @override - bool shouldRepaint(_EraserPainter oldDelegate) => - oldDelegate.position != position || oldDelegate.radius != radius; -} diff --git a/lib/main.dart b/lib/main.dart index 8c66d1c..998ffad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,9 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:memomap/app.dart'; +import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + // ignore: non_constant_identifier_names + String ACCESS_TOKEN = const String.fromEnvironment("ACCESS_TOKEN"); + MapboxOptions.setAccessToken(ACCESS_TOKEN); await dotenv.load(fileName: '.env'); runApp(const ProviderScope(child: App())); diff --git a/pubspec.lock b/pubspec.lock index efbe210..bd6b2d9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + benchmark: + dependency: transitive + description: + name: benchmark + sha256: cb3eeea01e3f054df76ee9775ca680f3afa5f19f39b2bb426ba78ba27654493b + url: "https://pub.dev" + source: hosted + version: "0.3.0" boolean_selector: dependency: transitive description: @@ -185,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + dart_sort_queue: + dependency: transitive + description: + name: dart_sort_queue + sha256: f3353ba8b4850e072d3368757f62edb79af34a9703c3e3df9c59342721f5f5b1 + url: "https://pub.dev" + source: hosted + version: "0.0.2+3" dart_style: dependency: transitive description: @@ -286,6 +302,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + url: "https://pub.dev" + source: hosted + version: "2.0.33" flutter_riverpod: dependency: "direct main" description: @@ -360,6 +384,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + geotypes: + dependency: transitive + description: + name: geotypes + sha256: "5bedf57de92283133dd221e363812ef50eaaba414f0823b1974ef7d84b86991f" + url: "https://pub.dev" + source: hosted + version: "0.0.2" glob: dependency: transitive description: @@ -592,6 +624,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + mapbox_maps_flutter: + dependency: "direct main" + description: + name: mapbox_maps_flutter + sha256: "982c7ef853190e16cdf06332ec1b5274b9c4e77cb35d81330aa84c4242faf4ad" + url: "https://pub.dev" + source: hosted + version: "2.19.1" matcher: dependency: transitive description: @@ -760,6 +800,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" + rbush: + dependency: transitive + description: + name: rbush + sha256: "48b683421b4afb43a642f82c6aa31911e54f3069143d31c7d33cbe329df13403" + url: "https://pub.dev" + source: hosted + version: "1.1.1" retrofit: dependency: "direct main" description: @@ -885,6 +933,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.42.0" + sweepline_intersections: + dependency: transitive + description: + name: sweepline_intersections + sha256: a665c707200a4f07140a4029b41a7c4883beb3f04322cd8e08ebf650f69e1176 + url: "https://pub.dev" + source: hosted + version: "0.0.4" term_glyph: dependency: transitive description: @@ -901,6 +957,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.7" + turf: + dependency: transitive + description: + name: turf + sha256: "75347c45a5c1de805db7cb182286f05a3770e01546626c4dc292709d15cbe436" + url: "https://pub.dev" + source: hosted + version: "0.0.10" + turf_equality: + dependency: transitive + description: + name: turf_equality + sha256: f0f44ffe389547941358e0d3d4a747db2bd56115b32ff1cede5e5bdf3126a3e2 + url: "https://pub.dev" + source: hosted + version: "0.1.0" + turf_pip: + dependency: transitive + description: + name: turf_pip + sha256: ba4fd414baffd5d7b30880658ad6db82461c49ec023f8ffd0c23d398ad8b14be + url: "https://pub.dev" + source: hosted + version: "0.0.2" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index be4e188..77690f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ environment: dependencies: flutter: sdk: flutter + mapbox_maps_flutter: ^2.19.1 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -80,6 +81,7 @@ flutter: assets: - .env + - assets/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images