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: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';

View file

@ -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
// **************************************************************************

View file

@ -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<DataPoint> 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<BitmapDescriptor> build(List<DataPoint> dataPoints) {
return _getClusterBitmap(
_getMarkerSize(dataPoints.length),
Future<BitmapDescriptor> 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<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) {
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<BitmapDescriptor> _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<BitmapDescriptor> 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<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 {

View file

@ -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 {

View file

@ -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: