diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5515e42..3dc4004 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -9,6 +9,8 @@ PODS: - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - PromisesObjC (~> 2.4) + - connectivity_plus (0.0.1): + - Flutter - Flutter (1.0.0) - flutter_secure_storage (6.0.0): - Flutter @@ -56,16 +58,21 @@ PODS: - Flutter - FlutterMacOS - PromisesObjC (2.4.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - Turf (4.0.0) - url_launcher_ios (0.0.1): - Flutter DEPENDENCIES: + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - 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`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -83,6 +90,8 @@ SPEC REPOS: - Turf EXTERNAL SOURCES: + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" Flutter: :path: Flutter flutter_secure_storage: @@ -93,12 +102,15 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/mapbox_maps_flutter/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9 @@ -112,6 +124,7 @@ SPEC CHECKSUMS: MapboxMaps: db05259e1ea68e9a8a4e6f9014027418025f2ebe path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb Turf: c9eb11a65d96af58cac523460fd40fec5061b081 url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b diff --git a/lib/features/map/presentation/map_screen.dart b/lib/features/map/presentation/map_screen.dart index 601a143..f6aff5b 100644 --- a/lib/features/map/presentation/map_screen.dart +++ b/lib/features/map/presentation/map_screen.dart @@ -7,6 +7,7 @@ import 'package:memomap/features/map/providers/current_map_provider.dart'; import 'package:memomap/features/map/providers/map_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/providers/map_bounds_provider.dart'; import 'package:memomap/features/map/models/drawing_path.dart'; import 'package:memomap/features/map/presentation/widgets/controls.dart'; import 'package:memomap/features/map/presentation/widgets/pin_list.dart'; @@ -14,6 +15,7 @@ import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; import 'package:flutter/services.dart'; import 'dart:math' as math; import 'dart:convert'; +import 'dart:async'; class MapScreen extends ConsumerStatefulWidget { const MapScreen({super.key}); @@ -37,12 +39,18 @@ class _MapScreenState extends ConsumerState { final Map _pinToAnnotation = {}; double? _cachedZoom; + Timer? _boundsUpdateTimer; @override void initState() { super.initState(); _loadPinImage(); MapboxMapsOptions.setLanguage("ja"); + + // 定期的に地図の表示範囲を更新 + _boundsUpdateTimer = Timer.periodic(const Duration(seconds: 1), (_) { + _updateMapBounds(); + }); } Future _loadPinImage() async { @@ -69,6 +77,7 @@ class _MapScreenState extends ConsumerState { ); _updatePins(); + _updateMapBounds(); setState(() {}); } @@ -80,6 +89,38 @@ class _MapScreenState extends ConsumerState { }); } + Future _updateMapBounds() async { + if (_mapboxMap == null || !mounted) return; + + // 画面サイズを取得 + final screenSize = MediaQuery.of(context).size; + + // 画面中心と左上のピクセル座標を緯度経度に変換 + final center = await _mapboxMap!.coordinateForPixel( + ScreenCoordinate(x: screenSize.width / 2, y: screenSize.height / 2), + ); + final topLeft = await _mapboxMap!.coordinateForPixel( + ScreenCoordinate(x: 0, y: 0), + ); + + // 中心から左上までの距離を計算(外接円の半径) + final centerLat = center.coordinates.lat; + final centerLng = center.coordinates.lng; + final topLeftLat = topLeft.coordinates.lat; + final topLeftLng = topLeft.coordinates.lng; + + final latDiff = centerLat - topLeftLat; + final lngDiff = centerLng - topLeftLng; + final radius = math.sqrt(latDiff * latDiff + lngDiff * lngDiff).toDouble(); + + final bounds = MapBounds( + center: LatLng(centerLat.toDouble(), centerLng.toDouble()), + radius: radius, + ); + + ref.read(mapBoundsProvider.notifier).state = bounds; + } + Future _updatePins({bool fullRebuild = false}) async { if (pointAnnotationManager == null || _pinImageData == null) return; @@ -118,10 +159,7 @@ class _MapScreenState extends ConsumerState { final annotation = await pointAnnotationManager!.create( PointAnnotationOptions( geometry: Point( - coordinates: Position( - pin.position.longitude, - pin.position.latitude, - ), + coordinates: Position(pin.position.longitude, pin.position.latitude), ), image: _pinImageData, iconSize: 0.5, @@ -208,7 +246,9 @@ class _MapScreenState extends ConsumerState { "width", ]); await style.setStyleLayerProperty( - "existing_paths_layer", "line-opacity", 1.0, + "existing_paths_layer", + "line-opacity", + 1.0, ); await style.addSource(GeoJsonSource(id: "current_path_source")); @@ -478,6 +518,7 @@ class _MapScreenState extends ConsumerState { @override void dispose() { + _boundsUpdateTimer?.cancel(); _annotationToPin.clear(); _pinToAnnotation.clear(); super.dispose(); @@ -497,7 +538,10 @@ class _MapScreenState extends ConsumerState { final isDrawingMode = drawingState?.isDrawingMode ?? false; final strokeWidth = drawingState?.strokeWidth ?? 3.0; - ref.listen(drawingProvider.select((s) => s.valueOrNull?.paths), (previous, next) { + ref.listen(drawingProvider.select((s) => s.valueOrNull?.paths), ( + previous, + next, + ) { _updateLines(); }); @@ -521,7 +565,10 @@ class _MapScreenState extends ConsumerState { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text(currentMap?.name ?? (currentMapId != null ? 'Loading...' : 'Memomap')), + Text( + currentMap?.name ?? + (currentMapId != null ? 'Loading...' : 'Memomap'), + ), const SizedBox(width: 4), const Icon(Icons.arrow_drop_down, size: 20), ], @@ -572,13 +619,11 @@ class _MapScreenState extends ConsumerState { onMapCreated: _onMapCreated, onStyleLoadedListener: _onStyleLoaded, onTapListener: _onMapTap, - gestureRecognizers: drawingState.isDrawingMode - ? {} - : null, + gestureRecognizers: isDrawingMode ? {} : null, ), if (_mapboxMap != null) IgnorePointer( - ignoring: !drawingState.isDrawingMode, + ignoring: !isDrawingMode, child: GestureDetector( behavior: HitTestBehavior.opaque, onPanStart: _onPanStart, @@ -590,14 +635,12 @@ class _MapScreenState extends ConsumerState { if (_eraserPosition != null) Positioned( left: - _eraserPosition!.dx - - drawingState.strokeWidth * 2, + _eraserPosition!.dx - strokeWidth * 2, top: - _eraserPosition!.dy - - drawingState.strokeWidth * 2, + _eraserPosition!.dy - strokeWidth * 2, child: Container( - width: drawingState.strokeWidth * 4, - height: drawingState.strokeWidth * 4, + width: strokeWidth * 4, + height: strokeWidth * 4, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( @@ -659,7 +702,9 @@ class _MapScreenState extends ConsumerState { const Controls(), ], ), - if (currentMap == null && currentMapId == null && !mapsAsync.isLoading) + if (currentMap == null && + currentMapId == null && + !mapsAsync.isLoading) Positioned( top: 16, left: 16, @@ -678,7 +723,9 @@ class _MapScreenState extends ConsumerState { const Icon(Icons.info_outline, color: Colors.amber), const SizedBox(width: 12), const Expanded( - child: Text('No map selected. Create or select a map to add pins and drawings.'), + child: Text( + 'No map selected. Create or select a map to add pins and drawings.', + ), ), TextButton( onPressed: () => context.push('/maps'), diff --git a/lib/features/map/presentation/widgets/controls.dart b/lib/features/map/presentation/widgets/controls.dart index c0c9628..0c86dc6 100644 --- a/lib/features/map/presentation/widgets/controls.dart +++ b/lib/features/map/presentation/widgets/controls.dart @@ -10,6 +10,10 @@ class Controls extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final drawingStateAsync = ref.watch(drawingProvider); final drawingState = drawingStateAsync.valueOrNull; + final isDrawingMode = drawingState?.isDrawingMode ?? false; + final isEraserMode = drawingState?.isEraserMode ?? false; + final selectedColor = drawingState?.selectedColor ?? Colors.red; + final strokeWidth = drawingState?.strokeWidth ?? 3; final drawingNotifier = ref.read(drawingProvider.notifier); final colorScheme = Theme.of(context).colorScheme; @@ -41,9 +45,7 @@ class Controls extends ConsumerWidget { Icon( Icons.explore, size: 60, - color: !drawingState.isDrawingMode - ? colorScheme.primary - : Colors.grey, + color: !isDrawingMode ? colorScheme.primary : Colors.grey, ), ], ), @@ -64,7 +66,7 @@ class Controls extends ConsumerWidget { ), ); }, - child: drawingState.isDrawingMode + child: isDrawingMode ? Container( key: const ValueKey('expanded_controls'), padding: const EdgeInsets.only(bottom: 24, top: 12), @@ -83,15 +85,13 @@ class Controls extends ConsumerWidget { IconButton( icon: Icon( MyFlutterApp.eraser_1, - color: drawingState.isEraserMode + color: isEraserMode ? colorScheme.primary : colorScheme.onSurface, ), tooltip: '消しゴム', - onPressed: () => - drawingNotifier.setEraserMode( - !drawingState.isEraserMode, - ), + onPressed: () => drawingNotifier + .setEraserMode(!isEraserMode), ), ], ), @@ -117,9 +117,8 @@ class Controls extends ConsumerWidget { (entry) => _ColorCircle( index: entry.key, isSelected: - !drawingState.isEraserMode && - drawingState.selectedColor == - entry.value, + !isEraserMode && + selectedColor == entry.value, color: entry.value, onTap: () => drawingNotifier .selectColor(entry.value), @@ -129,10 +128,10 @@ class Controls extends ConsumerWidget { ), const SizedBox(height: 10), _StrokeWidthSlider( - color: drawingState.isEraserMode + color: isEraserMode ? Colors.grey - : drawingState.selectedColor, - width: drawingState.strokeWidth, + : selectedColor, + width: strokeWidth, setWidth: (newWidth) => drawingNotifier .changeStrokeWidth(newWidth), ), diff --git a/lib/features/map/presentation/widgets/pin_list.dart b/lib/features/map/presentation/widgets/pin_list.dart index 5637fb0..f3f8534 100644 --- a/lib/features/map/presentation/widgets/pin_list.dart +++ b/lib/features/map/presentation/widgets/pin_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:memomap/features/map/providers/pin_provider.dart'; +import 'package:memomap/features/map/providers/map_bounds_provider.dart'; class PinList extends ConsumerWidget { const PinList({super.key, this.onSheetSizeChanged}); @@ -12,6 +13,7 @@ class PinList extends ConsumerWidget { final pinsAsync = ref.watch(pinsProvider); final pinsNotifier = ref.watch(pinsProvider.notifier); final colorScheme = Theme.of(context).colorScheme; + final mapBounds = ref.watch(mapBoundsProvider); return DraggableScrollableSheet( initialChildSize: 0.2, minChildSize: 0.05, @@ -43,6 +45,9 @@ class PinList extends ConsumerWidget { itemCount: pins.length, itemBuilder: (context, index) { final pin = pins[index]; + final isOutOfBounds = mapBounds != null && + !mapBounds.contains(pin.position); + return Dismissible( key: ValueKey(pin), onDismissed: (direction) { @@ -65,7 +70,31 @@ class PinList extends ConsumerWidget { }, child: ListTile( leading: Image.asset('assets/pin.png'), - title: Text('ピン'), + title: Row( + children: [ + const Text('ピン'), + if (isOutOfBounds) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: colorScheme.errorContainer, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '表示範囲外', + style: TextStyle( + fontSize: 10, + color: colorScheme.onErrorContainer, + ), + ), + ), + ], + ], + ), subtitle: Text( '緯度: ${pin.position.latitude.toStringAsFixed(4)}, 経度: ${pin.position.longitude.toStringAsFixed(4)}', ), diff --git a/lib/features/map/providers/map_bounds_provider.dart b/lib/features/map/providers/map_bounds_provider.dart new file mode 100644 index 0000000..5e683ac --- /dev/null +++ b/lib/features/map/providers/map_bounds_provider.dart @@ -0,0 +1,24 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:latlong2/latlong.dart'; +import 'dart:math' as math; + +class MapBounds { + final LatLng center; + final double radius; + + const MapBounds({ + required this.center, + required this.radius, + }); + + bool contains(LatLng point) { + // 中心からの距離を計算 + final latDiff = center.latitude - point.latitude; + final lngDiff = center.longitude - point.longitude; + final distance = math.sqrt(latDiff * latDiff + lngDiff * lngDiff); + + return distance <= radius; + } +} + +final mapBoundsProvider = StateProvider((ref) => null); diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 8ba400f..19ea7fb 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,27 +1,106 @@ PODS: + - AppAuth (1.7.6): + - AppAuth/Core (= 1.7.6) + - AppAuth/ExternalUserAgent (= 1.7.6) + - AppAuth/Core (1.7.6) + - AppAuth/ExternalUserAgent (1.7.6): + - AppAuth/Core + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - connectivity_plus (0.0.1): + - FlutterMacOS + - flutter_secure_storage_macos (6.1.3): + - FlutterMacOS - FlutterMacOS (1.0.0) + - google_sign_in_ios (0.0.1): + - AppAuth (>= 1.7.4) + - Flutter + - FlutterMacOS + - GoogleSignIn (~> 8.0) + - GTMSessionFetcher (>= 3.4.0) + - GoogleSignIn (8.0.0): + - AppAuth (< 2.0, >= 1.7.3) + - AppCheckCore (~> 11.0) + - GTMAppAuth (< 5.0, >= 4.1.1) + - GTMSessionFetcher/Core (~> 3.3) + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMAppAuth (4.1.1): + - AppAuth/Core (~> 1.7) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher (3.5.0): + - GTMSessionFetcher/Full (= 3.5.0) + - GTMSessionFetcher/Core (3.5.0) + - GTMSessionFetcher/Full (3.5.0): + - GTMSessionFetcher/Core - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - PromisesObjC (2.4.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS DEPENDENCIES: + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - google_sign_in_ios (from `Flutter/ephemeral/.symlinks/plugins/google_sign_in_ios/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) +SPEC REPOS: + https://github.com/CocoaPods/Specs.git: + - AppAuth + - AppCheckCore + - GoogleSignIn + - GoogleUtilities + - GTMAppAuth + - GTMSessionFetcher + - PromisesObjC + EXTERNAL SOURCES: + connectivity_plus: + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + flutter_secure_storage_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: :path: Flutter/ephemeral + google_sign_in_ios: + :path: Flutter/ephemeral/.symlinks/plugins/google_sign_in_ios/darwin path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: + AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f + connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e + flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9 + GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd PODFILE CHECKSUM: 979b4831285933932bf71b1b904cf61a612b6d3c diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 1f84891..fa815c2 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -241,6 +241,7 @@ 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, 3A9F1B595357D3C69AA4399E /* [CP] Embed Pods Frameworks */, + DB8E58E01DD20C38910FC5C5 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -422,6 +423,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + DB8E58E01DD20C38910FC5C5 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */