mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Update map marker to show image thumbnails
This commit is contained in:
parent
a9de0b315c
commit
fffe8f7d2b
5 changed files with 209 additions and 91 deletions
|
@ -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';
|
||||||
|
|
|
@ -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
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue