Update map marker to show image thumbnails

This commit is contained in:
Ming Ming 2024-07-24 23:23:48 +08:00
parent a9de0b315c
commit fffe8f7d2b
5 changed files with 209 additions and 91 deletions

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
@ -13,6 +14,7 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc_util.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/account_controller.dart';
import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/controller/pref_controller.dart';
import 'package:nc_photos/di_container.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/entity/image_location/repo.dart';
import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/k.dart' as k; 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/snack_bar_manager.dart';
import 'package:nc_photos/stream_extension.dart'; import 'package:nc_photos/stream_extension.dart';
import 'package:nc_photos/stream_util.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/collection_browser.dart';
import 'package:nc_photos/widget/measure.dart'; import 'package:nc_photos/widget/measure.dart';
import 'package:nc_photos/widget/navigation_bar_blur_filter.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_codegen/np_codegen.dart';
import 'package:np_common/object_util.dart'; import 'package:np_common/object_util.dart';
import 'package:np_datetime/np_datetime.dart'; import 'package:np_datetime/np_datetime.dart';

View file

@ -64,6 +64,20 @@ extension _$_BlocNpLog on _Bloc {
static final log = Logger("widget.map_browser._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 // ToStringGenerator
// ************************************************************************** // **************************************************************************

View file

@ -51,123 +51,196 @@ class _MarkerBuilder {
} }
final BuildContext context; 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 { class _OsmMarkerBuilder extends _MarkerBuilder {
_OsmMarkerBuilder(super.context); _OsmMarkerBuilder(
super.context, {
required this.account,
});
Widget build(List<DataPoint> dataPoints) { Widget build(List<_DataPoint> dataPoints) {
final text = _getMarkerCountString(dataPoints.length); final text = _getMarkerCountString(dataPoints.length);
return _OsmMarker( return _OsmMarker(
account: account,
fileId: dataPoints.first.fileId,
size: _getMarkerSize(dataPoints.length), size: _getMarkerSize(dataPoints.length),
color: Theme.of(context).colorScheme.primaryContainer,
text: text, text: text,
textSize: _getMarkerTextSize(text, dataPoints.length), textSize: _getMarkerTextSize(dataPoints.length),
color: _getMarkerColor(dataPoints.length), textColor: Theme.of(context).colorScheme.onPrimaryContainer,
); );
} }
double _getMarkerSize(int count) { double _getMarkerSize(int count) {
final r = _getMarkerRatio(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); final r = _getMarkerRatio(count);
return (r * 3) + 9 - ((text.length / 6) * 1); return (r * 3) + 7;
} }
Color _getMarkerColor(int count) { final Account account;
final r = _getMarkerRatio(count);
return HSLColor.lerp(_minColorHsl, _maxColorHsl, r)!.toColor();
}
} }
@npLog
class _GoogleMarkerBuilder extends _MarkerBuilder { class _GoogleMarkerBuilder extends _MarkerBuilder {
_GoogleMarkerBuilder(super.context); _GoogleMarkerBuilder(
super.context, {
required this.account,
});
Future<BitmapDescriptor> build(List<DataPoint> dataPoints) { Future<BitmapDescriptor> build(List<_DataPoint> dataPoints) async {
return _getClusterBitmap( return _GoogleMarkerBitmapBuilder(
_getMarkerSize(dataPoints.length), imagePath: await _getImagePath(dataPoints),
size: _getMarkerSize(dataPoints.length),
color: Theme.of(context).colorScheme.primaryContainer,
text: _getMarkerCountString(dataPoints.length), text: _getMarkerCountString(dataPoints.length),
color: _getMarkerColor(dataPoints.length), textSize: _getMarkerTextSize(dataPoints.length),
); textColor: Theme.of(context).colorScheme.onPrimaryContainer,
).build();
}
Future<String?> _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) { double _getMarkerSize(int count) {
final r = _getMarkerRatio(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); final r = _getMarkerRatio(count);
return HSLColor.lerp(_minColorHsl, _maxColorHsl, r)!.toColor(); return (r * 10.5) + 24.5;
} }
Future<BitmapDescriptor> _getClusterBitmap( final Account account;
double size, { }
String? text,
required Color color, @npLog
}) async { class _GoogleMarkerBitmapBuilder {
final PictureRecorder pictureRecorder = PictureRecorder(); _GoogleMarkerBitmapBuilder({
final Canvas canvas = Canvas(pictureRecorder); required this.imagePath,
final fillPaint = Paint()..color = color; required this.size,
final outlinePaint = Paint() required this.color,
..color = Colors.white.withOpacity(.75) required this.text,
..strokeWidth = size / 28 required this.textSize,
..style = PaintingStyle.stroke; required this.textColor,
const shadowPadding = 6.0; });
const shadowPaddingHalf = shadowPadding / 2;
final shadowPath = Path() Future<BitmapDescriptor> build() async {
..addOval( final pictureRecorder = PictureRecorder();
Rect.fromLTWH(0, 0, size - shadowPadding, size - shadowPadding)); final canvas = Canvas(pictureRecorder);
canvas.drawShadow(shadowPath, Colors.black, 1, false);
canvas.drawCircle( _drawBackgroundShadow(canvas);
Offset(size / 2 - shadowPaddingHalf, size / 2 - shadowPaddingHalf), canvas.clipPath(_makeBackgroundPath());
size / 2 - shadowPaddingHalf, _drawBackground(canvas);
fillPaint, if (imagePath != null) {
); await _drawImage(canvas);
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,
),
);
} }
_drawText(canvas);
_drawBorder(canvas);
final img = final img =
await pictureRecorder.endRecording().toImage(size.ceil(), size.ceil()); await pictureRecorder.endRecording().toImage(size.ceil(), size.ceil());
final data = await img.toByteData(format: ImageByteFormat.png) as ByteData; final data = await img.toByteData(format: ImageByteFormat.png) as ByteData;
return BitmapDescriptor.fromBytes(data.buffer.asUint8List()); 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<void> _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 { enum _DateRangeType {

View file

@ -50,9 +50,11 @@ class _MapViewState extends State<_MapView> {
); );
}, },
googleClusterBuilder: (context, dataPoints) => googleClusterBuilder: (context, dataPoints) =>
_GoogleMarkerBuilder(context).build(dataPoints), _GoogleMarkerBuilder(context, account: context.bloc.account)
.build(dataPoints.cast()),
osmClusterBuilder: (context, dataPoints) => osmClusterBuilder: (context, dataPoints) =>
_OsmMarkerBuilder(context).build(dataPoints), _OsmMarkerBuilder(context, account: context.bloc.account)
.build(dataPoints.cast()),
contentPadding: EdgeInsets.only( contentPadding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top, top: MediaQuery.of(context).padding.top,
bottom: MediaQuery.of(context).padding.bottom, bottom: MediaQuery.of(context).padding.bottom,
@ -75,10 +77,13 @@ class _MapViewState extends State<_MapView> {
class _OsmMarker extends StatelessWidget { class _OsmMarker extends StatelessWidget {
const _OsmMarker({ const _OsmMarker({
required this.account,
required this.fileId,
required this.size, required this.size,
required this.color,
required this.text, required this.text,
required this.textSize, required this.textSize,
required this.color, required this.textColor,
}); });
@override @override
@ -98,27 +103,47 @@ class _OsmMarker extends StatelessWidget {
], ],
border: Border.all( border: Border.all(
color: Colors.white.withOpacity(.75), color: Colors.white.withOpacity(.75),
width: 1.5, width: 2,
), ),
color: color, color: color,
), ),
child: Center( child: ClipRRect(
child: Text( borderRadius: BorderRadius.circular(size / 2),
text, child: Stack(
style: TextStyle( children: [
fontSize: textSize, NetworkRectThumbnail(
color: Colors.white.withOpacity(.75), 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 double size;
final Color color;
final String text; final String text;
final double textSize; final double textSize;
final Color color; final Color textColor;
} }
class _PanelContainer extends StatefulWidget { class _PanelContainer extends StatefulWidget {

View file

@ -18,6 +18,8 @@ dependencies:
latlong2: any latlong2: any
np_async: np_async:
path: ../np_async path: ../np_async
np_collection:
path: ../np_collection
np_common: np_common:
path: ../np_common path: ../np_common
np_platform_util: np_platform_util: