From fffe8f7d2b303730dd5b8bb22e990ab12759d665 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 24 Jul 2024 23:23:48 +0800 Subject: [PATCH] Update map marker to show image thumbnails --- app/lib/widget/map_browser.dart | 4 + app/lib/widget/map_browser.g.dart | 14 ++ app/lib/widget/map_browser/type.dart | 231 ++++++++++++++++++--------- app/lib/widget/map_browser/view.dart | 49 ++++-- np_gps_map/pubspec.yaml | 2 + 5 files changed, 209 insertions(+), 91 deletions(-) diff --git a/app/lib/widget/map_browser.dart b/app/lib/widget/map_browser.dart index 64c2986e..01e517b7 100644 --- a/app/lib/widget/map_browser.dart +++ b/app/lib/widget/map_browser.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:ui'; import 'package:clock/clock.dart'; @@ -13,6 +14,7 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/bloc_util.dart'; +import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/di_container.dart'; @@ -21,6 +23,7 @@ import 'package:nc_photos/entity/collection/content_provider/ad_hoc.dart'; import 'package:nc_photos/entity/image_location/repo.dart'; import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/k.dart' as k; +import 'package:nc_photos/np_api_util.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/stream_extension.dart'; import 'package:nc_photos/stream_util.dart'; @@ -29,6 +32,7 @@ import 'package:nc_photos/theme/dimension.dart'; import 'package:nc_photos/widget/collection_browser.dart'; import 'package:nc_photos/widget/measure.dart'; import 'package:nc_photos/widget/navigation_bar_blur_filter.dart'; +import 'package:nc_photos/widget/network_thumbnail.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/object_util.dart'; import 'package:np_datetime/np_datetime.dart'; diff --git a/app/lib/widget/map_browser.g.dart b/app/lib/widget/map_browser.g.dart index fe4e9a06..35fe8b8d 100644 --- a/app/lib/widget/map_browser.g.dart +++ b/app/lib/widget/map_browser.g.dart @@ -64,6 +64,20 @@ extension _$_BlocNpLog on _Bloc { static final log = Logger("widget.map_browser._Bloc"); } +extension _$_GoogleMarkerBuilderNpLog on _GoogleMarkerBuilder { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.map_browser._GoogleMarkerBuilder"); +} + +extension _$_GoogleMarkerBitmapBuilderNpLog on _GoogleMarkerBitmapBuilder { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.map_browser._GoogleMarkerBitmapBuilder"); +} + // ************************************************************************** // ToStringGenerator // ************************************************************************** diff --git a/app/lib/widget/map_browser/type.dart b/app/lib/widget/map_browser/type.dart index 3cccb040..80a8e96b 100644 --- a/app/lib/widget/map_browser/type.dart +++ b/app/lib/widget/map_browser/type.dart @@ -51,123 +51,196 @@ class _MarkerBuilder { } final BuildContext context; - - late final _minColorHsl = - HSLColor.fromColor(Theme.of(context).colorScheme.primary) - .withSaturation( - Theme.of(context).brightness == Brightness.light ? .9 : .7) - .withLightness( - Theme.of(context).brightness == Brightness.light ? .4 : .3); - late final _maxColorHsl = - HSLColor.fromColor(Theme.of(context).colorScheme.primary) - .withSaturation( - Theme.of(context).brightness == Brightness.light ? .9 : .7) - .withLightness( - Theme.of(context).brightness == Brightness.light ? .3 : .2); } class _OsmMarkerBuilder extends _MarkerBuilder { - _OsmMarkerBuilder(super.context); + _OsmMarkerBuilder( + super.context, { + required this.account, + }); - Widget build(List dataPoints) { + Widget build(List<_DataPoint> dataPoints) { final text = _getMarkerCountString(dataPoints.length); return _OsmMarker( + account: account, + fileId: dataPoints.first.fileId, size: _getMarkerSize(dataPoints.length), + color: Theme.of(context).colorScheme.primaryContainer, text: text, - textSize: _getMarkerTextSize(text, dataPoints.length), - color: _getMarkerColor(dataPoints.length), + textSize: _getMarkerTextSize(dataPoints.length), + textColor: Theme.of(context).colorScheme.onPrimaryContainer, ); } double _getMarkerSize(int count) { final r = _getMarkerRatio(count); - return (r * 28).toInt() + 28; + return (r * 30).toInt() + 55; } - double _getMarkerTextSize(String text, int count) { + double _getMarkerTextSize(int count) { final r = _getMarkerRatio(count); - return (r * 3) + 9 - ((text.length / 6) * 1); + return (r * 3) + 7; } - Color _getMarkerColor(int count) { - final r = _getMarkerRatio(count); - return HSLColor.lerp(_minColorHsl, _maxColorHsl, r)!.toColor(); - } + final Account account; } +@npLog class _GoogleMarkerBuilder extends _MarkerBuilder { - _GoogleMarkerBuilder(super.context); + _GoogleMarkerBuilder( + super.context, { + required this.account, + }); - Future build(List dataPoints) { - return _getClusterBitmap( - _getMarkerSize(dataPoints.length), + Future build(List<_DataPoint> dataPoints) async { + return _GoogleMarkerBitmapBuilder( + imagePath: await _getImagePath(dataPoints), + size: _getMarkerSize(dataPoints.length), + color: Theme.of(context).colorScheme.primaryContainer, text: _getMarkerCountString(dataPoints.length), - color: _getMarkerColor(dataPoints.length), - ); + textSize: _getMarkerTextSize(dataPoints.length), + textColor: Theme.of(context).colorScheme.onPrimaryContainer, + ).build(); + } + + Future _getImagePath(List<_DataPoint> dataPoints) async { + try { + final url = NetworkRectThumbnail.imageUrlForFileId( + account, dataPoints.first.fileId); + final fileInfo = await ThumbnailCacheManager.inst.getSingleFile( + url, + headers: { + "authorization": AuthUtil.fromAccount(account).toHeaderValue(), + }, + ); + return fileInfo.absolute.path; + } catch (e, stackTrace) { + _log.severe( + "[_getImagePath] Failed to get file path for fileId: ${dataPoints.first.fileId}", + e, + stackTrace); + return null; + } } double _getMarkerSize(int count) { final r = _getMarkerRatio(count); - return (r * 75).toInt() + 100; + return (r * 105).toInt() + 192; } - Color _getMarkerColor(int count) { + double _getMarkerTextSize(int count) { final r = _getMarkerRatio(count); - return HSLColor.lerp(_minColorHsl, _maxColorHsl, r)!.toColor(); + return (r * 10.5) + 24.5; } - Future _getClusterBitmap( - double size, { - String? text, - required Color color, - }) async { - final PictureRecorder pictureRecorder = PictureRecorder(); - final Canvas canvas = Canvas(pictureRecorder); - final fillPaint = Paint()..color = color; - final outlinePaint = Paint() - ..color = Colors.white.withOpacity(.75) - ..strokeWidth = size / 28 - ..style = PaintingStyle.stroke; - const shadowPadding = 6.0; - const shadowPaddingHalf = shadowPadding / 2; - final shadowPath = Path() - ..addOval( - Rect.fromLTWH(0, 0, size - shadowPadding, size - shadowPadding)); - canvas.drawShadow(shadowPath, Colors.black, 1, false); - canvas.drawCircle( - Offset(size / 2 - shadowPaddingHalf, size / 2 - shadowPaddingHalf), - size / 2 - shadowPaddingHalf, - fillPaint, - ); - canvas.drawCircle( - Offset(size / 2 - shadowPaddingHalf, size / 2 - shadowPaddingHalf), - size / 2 - shadowPaddingHalf - (size / 28 / 2), - outlinePaint, - ); - if (text != null) { - TextPainter painter = TextPainter(textDirection: TextDirection.ltr); - painter.text = TextSpan( - text: text, - style: TextStyle( - fontSize: size / 3 - ((text.length / 6) * (size * 0.1)), - color: Colors.white.withOpacity(.75), - fontWeight: FontWeight.normal, - ), - ); - painter.layout(); - painter.paint( - canvas, - Offset( - size / 2 - painter.width / 2 - shadowPaddingHalf, - size / 2 - painter.height / 2 - shadowPaddingHalf, - ), - ); + final Account account; +} + +@npLog +class _GoogleMarkerBitmapBuilder { + _GoogleMarkerBitmapBuilder({ + required this.imagePath, + required this.size, + required this.color, + required this.text, + required this.textSize, + required this.textColor, + }); + + Future build() async { + final pictureRecorder = PictureRecorder(); + final canvas = Canvas(pictureRecorder); + + _drawBackgroundShadow(canvas); + canvas.clipPath(_makeBackgroundPath()); + _drawBackground(canvas); + if (imagePath != null) { + await _drawImage(canvas); } + _drawText(canvas); + _drawBorder(canvas); + final img = await pictureRecorder.endRecording().toImage(size.ceil(), size.ceil()); final data = await img.toByteData(format: ImageByteFormat.png) as ByteData; return BitmapDescriptor.fromBytes(data.buffer.asUint8List()); } + + void _drawBackgroundShadow(Canvas canvas) { + final shadowPath = _makeBackgroundPath(); + canvas.drawShadow(shadowPath, Colors.black, 1, false); + } + + void _drawBackground(Canvas canvas) { + canvas.drawColor(color, BlendMode.src); + } + + Future _drawImage(Canvas canvas) async { + try { + final imageData = await File(imagePath!).readAsBytes(); + final imageDescriptor = await ImageDescriptor.encoded( + await ImmutableBuffer.fromUint8List(imageData), + ); + final codec = await imageDescriptor.instantiateCodec(); + final frame = await codec.getNextFrame(); + paintImage( + canvas: canvas, + rect: Rect.fromLTRB(0, 0, size - _shadowPadding, size - _shadowPadding), + image: frame.image, + fit: BoxFit.cover, + filterQuality: FilterQuality.low, + ); + } catch (e, stackTrace) { + _log.severe( + "[_drawImage] Failed to draw image: $imagePath", e, stackTrace); + } + } + + void _drawText(Canvas canvas) { + final textPaint = TextPainter(textDirection: TextDirection.ltr); + textPaint.text = TextSpan( + text: text, + style: TextStyle(fontSize: textSize, color: textColor), + ); + textPaint.layout(); + final y = size - textPaint.height - _shadowPadding - size * .07; + + final fillPaint = Paint()..color = color; + canvas.drawRect(Rect.fromLTRB(0, y - size * .04, size, size), fillPaint); + + textPaint.paint( + canvas, + Offset(size / 2 - textPaint.width / 2 - _shadowPaddingHalf, y), + ); + } + + void _drawBorder(Canvas canvas) { + final outlinePaint = Paint() + ..color = Color.alphaBlend(Colors.white.withOpacity(.75), color) + ..strokeWidth = size * .04 + ..style = PaintingStyle.stroke; + canvas.drawCircle( + Offset(size / 2 - _shadowPaddingHalf, size / 2 - _shadowPaddingHalf), + size / 2 - _shadowPaddingHalf - (size * .04 / 2), + outlinePaint, + ); + } + + Path _makeBackgroundPath() { + return Path() + ..addOval( + Rect.fromLTWH(0, 0, size - _shadowPadding, size - _shadowPadding)); + } + + final String? imagePath; + final double size; + final Color color; + final String text; + final double textSize; + final Color textColor; + + static const _shadowPadding = 6.0; + static const _shadowPaddingHalf = _shadowPadding / 2; } enum _DateRangeType { diff --git a/app/lib/widget/map_browser/view.dart b/app/lib/widget/map_browser/view.dart index 6e69d57a..0abf6b9b 100644 --- a/app/lib/widget/map_browser/view.dart +++ b/app/lib/widget/map_browser/view.dart @@ -50,9 +50,11 @@ class _MapViewState extends State<_MapView> { ); }, googleClusterBuilder: (context, dataPoints) => - _GoogleMarkerBuilder(context).build(dataPoints), + _GoogleMarkerBuilder(context, account: context.bloc.account) + .build(dataPoints.cast()), osmClusterBuilder: (context, dataPoints) => - _OsmMarkerBuilder(context).build(dataPoints), + _OsmMarkerBuilder(context, account: context.bloc.account) + .build(dataPoints.cast()), contentPadding: EdgeInsets.only( top: MediaQuery.of(context).padding.top, bottom: MediaQuery.of(context).padding.bottom, @@ -75,10 +77,13 @@ class _MapViewState extends State<_MapView> { class _OsmMarker extends StatelessWidget { const _OsmMarker({ + required this.account, + required this.fileId, required this.size, + required this.color, required this.text, required this.textSize, - required this.color, + required this.textColor, }); @override @@ -98,27 +103,47 @@ class _OsmMarker extends StatelessWidget { ], border: Border.all( color: Colors.white.withOpacity(.75), - width: 1.5, + width: 2, ), color: color, ), - child: Center( - child: Text( - text, - style: TextStyle( - fontSize: textSize, - color: Colors.white.withOpacity(.75), - ), + child: ClipRRect( + borderRadius: BorderRadius.circular(size / 2), + child: Stack( + children: [ + NetworkRectThumbnail( + account: account, + imageUrl: + NetworkRectThumbnail.imageUrlForFileId(account, fileId), + errorBuilder: (_) => const SizedBox.shrink(), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(4, 1, 4, 1), + color: color, + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle(fontSize: textSize, color: textColor), + ), + ), + ), + ], ), ), ), ); } + final Account account; + final int fileId; final double size; + final Color color; final String text; final double textSize; - final Color color; + final Color textColor; } class _PanelContainer extends StatefulWidget { diff --git a/np_gps_map/pubspec.yaml b/np_gps_map/pubspec.yaml index 000a7cb5..3c7a20ee 100644 --- a/np_gps_map/pubspec.yaml +++ b/np_gps_map/pubspec.yaml @@ -18,6 +18,8 @@ dependencies: latlong2: any np_async: path: ../np_async + np_collection: + path: ../np_collection np_common: path: ../np_common np_platform_util: