mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Create abstraction and move gmap backend for map browser to package
This commit is contained in:
parent
14140c5ef0
commit
812c8eee9c
18 changed files with 431 additions and 265 deletions
|
@ -75,7 +75,7 @@ Future<void> init(InitIsolateType isolateType) async {
|
|||
}
|
||||
await _initDiContainer(isolateType);
|
||||
_initVisibilityDetector();
|
||||
GpsMap.init();
|
||||
initGpsMap();
|
||||
// init session storage
|
||||
SessionStorage();
|
||||
|
||||
|
|
|
@ -161,10 +161,12 @@ class PrefController {
|
|||
value: value,
|
||||
);
|
||||
|
||||
Future<bool> setMapBrowserPrevPosition(MapCoord value) => _set<MapCoord?>(
|
||||
Future<bool> setMapBrowserPrevPosition(MapCoord? value) =>
|
||||
_setOrRemove<MapCoord>(
|
||||
controller: _mapBrowserPrevPositionController,
|
||||
setter: (pref, value) => pref.setMapBrowserPrevPosition(
|
||||
jsonEncode([value!.latitude, value.longitude])),
|
||||
jsonEncode([value.latitude, value.longitude])),
|
||||
remover: (pref) => pref.setMapBrowserPrevPosition(null),
|
||||
value: value,
|
||||
);
|
||||
|
||||
|
@ -270,9 +272,9 @@ class PrefController {
|
|||
BehaviorSubject.seeded(_c.pref.isDontShowVideoPreviewHintOr(false));
|
||||
@npSubjectAccessor
|
||||
late final _mapBrowserPrevPositionController = BehaviorSubject.seeded(_c.pref
|
||||
.getMapBrowserPrevPosition()
|
||||
?.let(tryJsonDecode)
|
||||
?.let(_tryMapCoordFromJson));
|
||||
.getMapBrowserPrevPosition()
|
||||
?.let(tryJsonDecode)
|
||||
?.let(_tryMapCoordFromJson));
|
||||
}
|
||||
|
||||
@npSubjectAccessor
|
||||
|
|
|
@ -91,8 +91,13 @@ extension on Pref {
|
|||
|
||||
String? getMapBrowserPrevPosition() =>
|
||||
provider.getString(PrefKey.mapBrowserPrevPosition);
|
||||
Future<bool> setMapBrowserPrevPosition(String value) =>
|
||||
provider.setString(PrefKey.mapBrowserPrevPosition, value);
|
||||
Future<bool> setMapBrowserPrevPosition(String? value) {
|
||||
if (value == null) {
|
||||
return provider.remove(PrefKey.mapBrowserPrevPosition);
|
||||
} else {
|
||||
return provider.setString(PrefKey.mapBrowserPrevPosition, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MapCoord? _tryMapCoordFromJson(dynamic json) {
|
||||
|
|
|
@ -28,4 +28,10 @@ extension StreamExtension<T> on Stream<T> {
|
|||
|
||||
Stream<T> distinctBy<U>(U Function(T element) keyOf) =>
|
||||
distinct((previous, next) => keyOf(previous) == keyOf(next));
|
||||
|
||||
/// By default the first event will always pass through the distinct check,
|
||||
/// which may not always make sense. In this variant, the first event will be
|
||||
/// absorbed
|
||||
Stream<T> distinctByIgnoreFirst<U>(U Function(T element) keyOf) =>
|
||||
distinct((previous, next) => keyOf(previous) == keyOf(next)).skip(1);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import 'package:copy_with/copy_with.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
|
|
|
@ -16,7 +16,6 @@ abstract class $_StateCopyWithWorker {
|
|||
_State call(
|
||||
{List<_DataPoint>? data,
|
||||
MapCoord? initialPoint,
|
||||
Set<Marker>? markers,
|
||||
bool? isShowDataRangeControlPanel,
|
||||
_DateRangeType? dateRangeType,
|
||||
DateRange? localDateRange,
|
||||
|
@ -30,7 +29,6 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
|||
_State call(
|
||||
{dynamic data,
|
||||
dynamic initialPoint = copyWithNull,
|
||||
dynamic markers,
|
||||
dynamic isShowDataRangeControlPanel,
|
||||
dynamic dateRangeType,
|
||||
dynamic localDateRange,
|
||||
|
@ -40,7 +38,6 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
|||
initialPoint: initialPoint == copyWithNull
|
||||
? that.initialPoint
|
||||
: initialPoint as MapCoord?,
|
||||
markers: markers as Set<Marker>? ?? that.markers,
|
||||
isShowDataRangeControlPanel: isShowDataRangeControlPanel as bool? ??
|
||||
that.isShowDataRangeControlPanel,
|
||||
dateRangeType: dateRangeType as _DateRangeType? ?? that.dateRangeType,
|
||||
|
@ -74,7 +71,7 @@ extension _$_BlocNpLog on _Bloc {
|
|||
extension _$_StateToString on _State {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_State {data: [length: ${data.length}], initialPoint: $initialPoint, markers: {length: ${markers.length}}, isShowDataRangeControlPanel: $isShowDataRangeControlPanel, dateRangeType: ${dateRangeType.name}, localDateRange: $localDateRange, error: $error}";
|
||||
return "_State {data: [length: ${data.length}], initialPoint: $initialPoint, isShowDataRangeControlPanel: $isShowDataRangeControlPanel, dateRangeType: ${dateRangeType.name}, localDateRange: $localDateRange, error: $error}";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,13 +82,6 @@ extension _$_LoadDataToString on _LoadData {
|
|||
}
|
||||
}
|
||||
|
||||
extension _$_SetMarkersToString on _SetMarkers {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetMarkers {markers: {length: ${markers.length}}}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_OpenDataRangeControlPanelToString on _OpenDataRangeControlPanel {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
|
|
|
@ -13,15 +13,15 @@ class _Bloc extends Bloc<_Event, _State>
|
|||
_calcDateRange(clock.now().toDate(), _DateRangeType.thisMonth),
|
||||
)) {
|
||||
on<_LoadData>(_onLoadData);
|
||||
on<_SetMarkers>(_onSetMarkers);
|
||||
on<_OpenDataRangeControlPanel>(_onOpenDataRangeControlPanel);
|
||||
on<_CloseControlPanel>(_onCloseControlPanel);
|
||||
on<_SetDateRangeType>(_onSetDateRangeType);
|
||||
on<_SetLocalDateRange>(_onSetDateRange);
|
||||
on<_SetError>(_onSetError);
|
||||
|
||||
_subscriptions
|
||||
.add(stream.distinctBy((state) => state.localDateRange).listen((state) {
|
||||
_subscriptions.add(stream
|
||||
.distinctByIgnoreFirst((state) => state.localDateRange)
|
||||
.listen((state) {
|
||||
add(const _LoadData());
|
||||
}));
|
||||
}
|
||||
|
@ -77,11 +77,6 @@ class _Bloc extends Bloc<_Event, _State>
|
|||
}
|
||||
}
|
||||
|
||||
void _onSetMarkers(_SetMarkers ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(markers: ev.markers));
|
||||
}
|
||||
|
||||
void _onOpenDataRangeControlPanel(
|
||||
_OpenDataRangeControlPanel ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
|
|
|
@ -6,7 +6,6 @@ class _State {
|
|||
const _State({
|
||||
required this.data,
|
||||
this.initialPoint,
|
||||
required this.markers,
|
||||
required this.isShowDataRangeControlPanel,
|
||||
required this.dateRangeType,
|
||||
required this.localDateRange,
|
||||
|
@ -19,7 +18,6 @@ class _State {
|
|||
}) {
|
||||
return _State(
|
||||
data: const [],
|
||||
markers: const {},
|
||||
isShowDataRangeControlPanel: false,
|
||||
dateRangeType: dateRangeType,
|
||||
localDateRange: localDateRange,
|
||||
|
@ -31,7 +29,6 @@ class _State {
|
|||
|
||||
final List<_DataPoint> data;
|
||||
final MapCoord? initialPoint;
|
||||
final Set<Marker> markers;
|
||||
|
||||
final bool isShowDataRangeControlPanel;
|
||||
final _DateRangeType dateRangeType;
|
||||
|
@ -52,16 +49,6 @@ class _LoadData implements _Event {
|
|||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _SetMarkers implements _Event {
|
||||
const _SetMarkers(this.markers);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Set<Marker> markers;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _OpenDataRangeControlPanel implements _Event {
|
||||
const _OpenDataRangeControlPanel();
|
||||
|
|
|
@ -1,25 +1,155 @@
|
|||
part of '../map_browser.dart';
|
||||
|
||||
class _DataPoint implements ClusterItem {
|
||||
class _DataPoint extends DataPoint {
|
||||
const _DataPoint({
|
||||
required this.location,
|
||||
required super.position,
|
||||
required this.fileId,
|
||||
});
|
||||
|
||||
factory _DataPoint.fromImageLatLng(ImageLatLng src) => _DataPoint(
|
||||
location: LatLng(src.latitude, src.longitude),
|
||||
position: MapCoord(src.latitude, src.longitude),
|
||||
fileId: src.fileId,
|
||||
);
|
||||
|
||||
@override
|
||||
String get geohash =>
|
||||
Geohash.encode(location, codeLength: ClusterManager.precision);
|
||||
|
||||
@override
|
||||
final LatLng location;
|
||||
final int fileId;
|
||||
}
|
||||
|
||||
class _GoogleMarkerBuilder {
|
||||
_GoogleMarkerBuilder(this.context);
|
||||
|
||||
Future<BitmapDescriptor> build(List<DataPoint> dataPoints) {
|
||||
return _getClusterBitmap(
|
||||
_getMarkerSize(dataPoints.length),
|
||||
text: _getMarkerCountString(dataPoints.length),
|
||||
color: _getMarkerColor(dataPoints.length),
|
||||
);
|
||||
}
|
||||
|
||||
String _getMarkerCountString(int count) {
|
||||
switch (count) {
|
||||
case >= 10000:
|
||||
return "10000+";
|
||||
case >= 1000:
|
||||
return "${count ~/ 1000 * 1000}+";
|
||||
case >= 100:
|
||||
return "${count ~/ 100 * 100}+";
|
||||
case >= 10:
|
||||
return "${count ~/ 10 * 10}+";
|
||||
default:
|
||||
return count.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Color _getMarkerColor(int count) {
|
||||
const step = 1 / 4;
|
||||
final double r;
|
||||
switch (count) {
|
||||
case >= 10000:
|
||||
r = 1;
|
||||
case >= 1000:
|
||||
r = (count ~/ 1000) / 10 * step + step * 3;
|
||||
case >= 100:
|
||||
r = (count ~/ 100) / 10 * step + step * 2;
|
||||
case >= 10:
|
||||
r = (count ~/ 10) / 10 * step + step;
|
||||
default:
|
||||
r = (count / 10) * step;
|
||||
}
|
||||
if (Theme.of(context).brightness == Brightness.light) {
|
||||
return HSLColor.fromAHSL(
|
||||
1,
|
||||
_colorHsl.hue,
|
||||
r * .7 + .3,
|
||||
(_colorHsl.lightness - (.1 - r * .1)).clamp(0, 1),
|
||||
).toColor();
|
||||
} else {
|
||||
return HSLColor.fromAHSL(
|
||||
1,
|
||||
_colorHsl.hue,
|
||||
r * .6 + .4,
|
||||
(_colorHsl.lightness - (.1 - r * .1)).clamp(0, 1),
|
||||
).toColor();
|
||||
}
|
||||
}
|
||||
|
||||
int _getMarkerSize(int count) {
|
||||
const step = 1 / 4;
|
||||
final double r;
|
||||
switch (count) {
|
||||
case >= 10000:
|
||||
r = 1;
|
||||
case >= 1000:
|
||||
r = (count ~/ 1000) / 10 * step + step * 3;
|
||||
case >= 100:
|
||||
r = (count ~/ 100) / 10 * step + step * 2;
|
||||
case >= 10:
|
||||
r = (count ~/ 10) / 10 * step + step;
|
||||
default:
|
||||
r = (count / 10) * step;
|
||||
}
|
||||
return (r * 85).toInt() + 85;
|
||||
}
|
||||
|
||||
Future<BitmapDescriptor> _getClusterBitmap(
|
||||
int 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 = Theme.of(context).brightness == Brightness.light
|
||||
? Colors.black.withOpacity(.28)
|
||||
: Colors.white.withOpacity(.6)
|
||||
..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: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
);
|
||||
painter.layout();
|
||||
painter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
size / 2 - painter.width / 2 - shadowPaddingHalf,
|
||||
size / 2 - painter.height / 2 - shadowPaddingHalf,
|
||||
),
|
||||
);
|
||||
}
|
||||
final img = await pictureRecorder.endRecording().toImage(size, size);
|
||||
final data = await img.toByteData(format: ImageByteFormat.png) as ByteData;
|
||||
return BitmapDescriptor.fromBytes(data.buffer.asUint8List());
|
||||
}
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
late final _colorHsl =
|
||||
HSLColor.fromColor(Theme.of(context).colorScheme.primaryContainer);
|
||||
}
|
||||
|
||||
enum _DateRangeType {
|
||||
thisMonth,
|
||||
prevMonth,
|
||||
|
@ -40,7 +170,3 @@ enum _DateRangeType {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on MapCoord {
|
||||
LatLng toLatLng() => LatLng(latitude, longitude);
|
||||
}
|
||||
|
|
|
@ -12,212 +12,60 @@ class _MapViewState extends State<_MapView> {
|
|||
Widget build(BuildContext context) {
|
||||
return MultiBlocListener(
|
||||
listeners: [
|
||||
_BlocListenerT<List<_DataPoint>>(
|
||||
selector: (state) => state.data,
|
||||
listener: (context, data) {
|
||||
_clusterManager.setItems(data);
|
||||
},
|
||||
),
|
||||
_BlocListenerT<MapCoord?>(
|
||||
selector: (state) => state.initialPoint,
|
||||
listener: (context, initialPoint) {
|
||||
if (initialPoint != null) {
|
||||
_mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(initialPoint.toLatLng(), 10));
|
||||
_controller?.setPosition(initialPoint);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: _BlocBuilder(
|
||||
buildWhen: (previous, current) => previous.markers != current.markers,
|
||||
builder: (context, state) => GoogleMap(
|
||||
mapType: MapType.normal,
|
||||
initialCameraPosition: context
|
||||
.read<PrefController>()
|
||||
.mapBrowserPrevPositionValue
|
||||
?.let(
|
||||
(p) => CameraPosition(target: p.toLatLng(), zoom: 10)) ??
|
||||
const CameraPosition(target: LatLng(0, 0)),
|
||||
markers: state.markers,
|
||||
onMapCreated: (controller) {
|
||||
_clusterManager.setMapId(controller.mapId);
|
||||
_mapController = controller;
|
||||
if (Theme.of(context).brightness == Brightness.dark) {
|
||||
controller.setMapStyle(_mapStyleNight);
|
||||
}
|
||||
if (state.initialPoint != null) {
|
||||
controller.animateCamera(CameraUpdate.newLatLngZoom(
|
||||
state.initialPoint!.toLatLng(), 10));
|
||||
}
|
||||
},
|
||||
onCameraMove: _clusterManager.onCameraMove,
|
||||
onCameraIdle: _clusterManager.updateMap,
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top,
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
),
|
||||
buildWhen: (previous, current) => previous.data != current.data,
|
||||
builder: (context, state) {
|
||||
final prevPosition =
|
||||
context.read<PrefController>().mapBrowserPrevPositionValue;
|
||||
return InteractiveMap(
|
||||
providerHint: GpsMapProvider.google,
|
||||
initialPosition: prevPosition ?? const MapCoord(0, 0),
|
||||
initialZoom: prevPosition == null ? 2.5 : 10,
|
||||
dataPoints: state.data,
|
||||
onClusterTap: (dataPoints) {
|
||||
final c = Collection(
|
||||
name: "",
|
||||
contentProvider: CollectionAdHocProvider(
|
||||
account: context.bloc.account,
|
||||
fileIds: dataPoints
|
||||
.cast<_DataPoint>()
|
||||
.map((e) => e.fileId)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pushNamed(
|
||||
CollectionBrowser.routeName,
|
||||
arguments: CollectionBrowserArguments(c),
|
||||
);
|
||||
},
|
||||
googleClusterBuilder: (context, dataPoints) =>
|
||||
_GoogleMarkerBuilder(context).build(dataPoints),
|
||||
contentPadding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top,
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
onMapCreated: (controller) {
|
||||
_controller = controller;
|
||||
if (state.initialPoint != null) {
|
||||
controller.setPosition(state.initialPoint!);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<BitmapDescriptor> _getClusterBitmap(
|
||||
int 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 = Theme.of(context).brightness == Brightness.light
|
||||
? Colors.black.withOpacity(.28)
|
||||
: Colors.white.withOpacity(.6)
|
||||
..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: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
);
|
||||
painter.layout();
|
||||
painter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
size / 2 - painter.width / 2 - shadowPaddingHalf,
|
||||
size / 2 - painter.height / 2 - shadowPaddingHalf,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final img = await pictureRecorder.endRecording().toImage(size, size);
|
||||
final data = await img.toByteData(format: ImageByteFormat.png) as ByteData;
|
||||
|
||||
return BitmapDescriptor.fromBytes(data.buffer.asUint8List());
|
||||
}
|
||||
|
||||
String _getMarkerCountString(int count) {
|
||||
switch (count) {
|
||||
case >= 10000:
|
||||
return "10000+";
|
||||
case >= 1000:
|
||||
return "${count ~/ 1000 * 1000}+";
|
||||
case >= 100:
|
||||
return "${count ~/ 100 * 100}+";
|
||||
case >= 10:
|
||||
return "${count ~/ 10 * 10}+";
|
||||
default:
|
||||
return count.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Color _getMarkerColor(int count) {
|
||||
const step = 1 / 4;
|
||||
final double r;
|
||||
switch (count) {
|
||||
case >= 10000:
|
||||
r = 1;
|
||||
case >= 1000:
|
||||
r = (count ~/ 1000) / 10 * step + step * 3;
|
||||
case >= 100:
|
||||
r = (count ~/ 100) / 10 * step + step * 2;
|
||||
case >= 10:
|
||||
r = (count ~/ 10) / 10 * step + step;
|
||||
default:
|
||||
r = (count / 10) * step;
|
||||
}
|
||||
if (Theme.of(context).brightness == Brightness.light) {
|
||||
return HSLColor.fromAHSL(
|
||||
1,
|
||||
_colorHsl.hue,
|
||||
r * .7 + .3,
|
||||
(_colorHsl.lightness - (.1 - r * .1)).clamp(0, 1),
|
||||
).toColor();
|
||||
} else {
|
||||
return HSLColor.fromAHSL(
|
||||
1,
|
||||
_colorHsl.hue,
|
||||
r * .6 + .4,
|
||||
(_colorHsl.lightness - (.1 - r * .1)).clamp(0, 1),
|
||||
).toColor();
|
||||
}
|
||||
}
|
||||
|
||||
int _getMarkerSize(int count) {
|
||||
const step = 1 / 4;
|
||||
final double r;
|
||||
switch (count) {
|
||||
case >= 10000:
|
||||
r = 1;
|
||||
case >= 1000:
|
||||
r = (count ~/ 1000) / 10 * step + step * 3;
|
||||
case >= 100:
|
||||
r = (count ~/ 100) / 10 * step + step * 2;
|
||||
case >= 10:
|
||||
r = (count ~/ 10) / 10 * step + step;
|
||||
default:
|
||||
r = (count / 10) * step;
|
||||
}
|
||||
return (r * 85).toInt() + 85;
|
||||
}
|
||||
|
||||
late final _clusterManager = ClusterManager<_DataPoint>(
|
||||
const [],
|
||||
(markers) {
|
||||
if (mounted) {
|
||||
context.addEvent(_SetMarkers(markers));
|
||||
}
|
||||
},
|
||||
markerBuilder: (cluster) async => Marker(
|
||||
markerId: MarkerId(cluster.getId()),
|
||||
position: cluster.location,
|
||||
onTap: () {
|
||||
final c = Collection(
|
||||
name: "",
|
||||
contentProvider: CollectionAdHocProvider(
|
||||
account: context.bloc.account,
|
||||
fileIds: cluster.items.map((e) => e.fileId).toList(),
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pushNamed(
|
||||
CollectionBrowser.routeName,
|
||||
arguments: CollectionBrowserArguments(c),
|
||||
);
|
||||
},
|
||||
icon: await _getClusterBitmap(
|
||||
_getMarkerSize(cluster.count * 1),
|
||||
text: _getMarkerCountString(cluster.count * 1),
|
||||
color: _getMarkerColor(cluster.count * 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
GoogleMapController? _mapController;
|
||||
|
||||
late final _colorHsl =
|
||||
HSLColor.fromColor(Theme.of(context).colorScheme.primaryContainer);
|
||||
InteractiveMapController? _controller;
|
||||
}
|
||||
|
||||
class _PanelContainer extends StatefulWidget {
|
||||
|
@ -430,7 +278,7 @@ class _DateFieldState extends State<_DateField> {
|
|||
onTap: () async {
|
||||
final result = await showDatePicker(
|
||||
context: context,
|
||||
firstDate: DateTime(1970),
|
||||
firstDate: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
lastDate: clock.now(),
|
||||
currentDate: widget.date.toLocalDateTime(),
|
||||
);
|
||||
|
@ -458,7 +306,3 @@ class _DateFieldState extends State<_DateField> {
|
|||
|
||||
late final _controller = TextEditingController(text: _stringify(widget.date));
|
||||
}
|
||||
|
||||
// Generated in https://mapstyle.withgoogle.com/
|
||||
const _mapStyleNight =
|
||||
'[{"elementType":"geometry","stylers":[{"color":"#242f3e"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#746855"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#242f3e"}]},{"featureType":"administrative.locality","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#263c3f"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#6b9a76"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#38414e"}]},{"featureType":"road","elementType":"geometry.stroke","stylers":[{"color":"#212a37"}]},{"featureType":"road","elementType":"labels.text.fill","stylers":[{"color":"#9ca5b3"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#746855"}]},{"featureType":"road.highway","elementType":"geometry.stroke","stylers":[{"color":"#1f2835"}]},{"featureType":"road.highway","elementType":"labels.text.fill","stylers":[{"color":"#f3d19c"}]},{"featureType":"transit","elementType":"geometry","stylers":[{"color":"#2f3948"}]},{"featureType":"transit.station","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#17263c"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#515c6d"}]},{"featureType":"water","elementType":"labels.text.stroke","stylers":[{"color":"#17263c"}]}]';
|
||||
|
|
|
@ -661,7 +661,7 @@ packages:
|
|||
source: hosted
|
||||
version: "6.3.0"
|
||||
google_maps_cluster_manager:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_maps_cluster_manager
|
||||
sha256: "36e9a4b2d831c470fc85d692a6c9cec70e0f385d578b9697de5f4de347561b83"
|
||||
|
|
|
@ -159,7 +159,6 @@ dependencies:
|
|||
woozy_search: ^2.0.3
|
||||
|
||||
google_maps_flutter: 2.5.3
|
||||
google_maps_cluster_manager: 3.1.0
|
||||
|
||||
dependency_overrides:
|
||||
video_player:
|
||||
|
@ -177,6 +176,7 @@ dependency_overrides:
|
|||
url: https://gitlab.com/nc-photos/flutter-plugins
|
||||
ref: video_player-v2.8.6-nc-photos-2
|
||||
path: packages/video_player/video_player_platform_interface
|
||||
# fix google_maps_cluster_manager
|
||||
google_maps_flutter_android: 2.7.0
|
||||
google_maps_flutter_platform_interface: 2.5.0
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
library np_gps_map;
|
||||
|
||||
export 'src/gps_map.dart';
|
||||
export 'src/interactive_map.dart';
|
||||
export 'src/map_coord.dart';
|
||||
export 'src/util.dart' show initGpsMap;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:np_gps_map/src/map_coord.dart';
|
||||
import 'package:np_gps_map/src/native.dart';
|
||||
import 'package:np_gps_map/src/native/google_gps_map.dart'
|
||||
if (dart.library.html) 'package:np_gps_map/src/web/google_gps_map.dart';
|
||||
import 'package:np_gps_map/src/osm_gps_map.dart';
|
||||
import 'package:np_gps_map/src/util.dart';
|
||||
import 'package:np_platform_util/np_platform_util.dart';
|
||||
|
||||
enum GpsMapProvider {
|
||||
|
@ -21,16 +21,10 @@ class GpsMap extends StatelessWidget {
|
|||
this.onTap,
|
||||
});
|
||||
|
||||
static void init() {
|
||||
if (getRawPlatform() == NpPlatform.android) {
|
||||
Native.isNewGMapsRenderer().then((value) => _isNewGMapsRenderer = value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (providerHint == GpsMapProvider.osm ||
|
||||
(getRawPlatform() == NpPlatform.android && !_isNewGMapsRenderer)) {
|
||||
(getRawPlatform() == NpPlatform.android && !isNewGMapsRenderer())) {
|
||||
return OsmGpsMap(
|
||||
center: center,
|
||||
zoom: zoom,
|
||||
|
@ -53,6 +47,4 @@ class GpsMap extends StatelessWidget {
|
|||
final MapCoord center;
|
||||
final double zoom;
|
||||
final void Function()? onTap;
|
||||
|
||||
static bool _isNewGMapsRenderer = false;
|
||||
}
|
||||
|
|
62
np_gps_map/lib/src/interactive_map.dart
Normal file
62
np_gps_map/lib/src/interactive_map.dart
Normal file
|
@ -0,0 +1,62 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:np_gps_map/src/gps_map.dart';
|
||||
import 'package:np_gps_map/src/interactive_map/google.dart';
|
||||
import 'package:np_gps_map/src/map_coord.dart';
|
||||
import 'package:np_gps_map/src/util.dart';
|
||||
import 'package:np_platform_util/np_platform_util.dart';
|
||||
|
||||
// Client may extend this class to add custom data
|
||||
class DataPoint {
|
||||
const DataPoint({
|
||||
required this.position,
|
||||
});
|
||||
|
||||
final MapCoord position;
|
||||
}
|
||||
|
||||
abstract class InteractiveMapController {
|
||||
void setPosition(MapCoord position);
|
||||
}
|
||||
|
||||
class InteractiveMap extends StatelessWidget {
|
||||
const InteractiveMap({
|
||||
super.key,
|
||||
required this.providerHint,
|
||||
this.initialPosition,
|
||||
this.initialZoom,
|
||||
this.dataPoints,
|
||||
this.onClusterTap,
|
||||
this.googleClusterBuilder,
|
||||
this.contentPadding,
|
||||
this.onMapCreated,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (providerHint == GpsMapProvider.osm ||
|
||||
(getRawPlatform() == NpPlatform.android && !isNewGMapsRenderer())) {
|
||||
return const SizedBox.shrink();
|
||||
} else {
|
||||
return GoogleInteractiveMap(
|
||||
initialPosition: initialPosition,
|
||||
initialZoom: initialZoom,
|
||||
dataPoints: dataPoints,
|
||||
onClusterTap: onClusterTap,
|
||||
clusterBuilder: googleClusterBuilder,
|
||||
contentPadding: contentPadding,
|
||||
onMapCreated: onMapCreated,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The backend to provide the actual map. This works as a hint only, the
|
||||
/// actual choice may be different depending on the runtime environment
|
||||
final GpsMapProvider providerHint;
|
||||
final MapCoord? initialPosition;
|
||||
final double? initialZoom;
|
||||
final List<DataPoint>? dataPoints;
|
||||
final void Function(List<DataPoint> dataPoints)? onClusterTap;
|
||||
final GoogleClusterBuilder? googleClusterBuilder;
|
||||
final EdgeInsets? contentPadding;
|
||||
final void Function(InteractiveMapController controller)? onMapCreated;
|
||||
}
|
137
np_gps_map/lib/src/interactive_map/google.dart
Normal file
137
np_gps_map/lib/src/interactive_map/google.dart
Normal file
|
@ -0,0 +1,137 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:np_common/object_util.dart';
|
||||
import 'package:np_gps_map/src/interactive_map.dart';
|
||||
import 'package:np_gps_map/src/map_coord.dart';
|
||||
|
||||
typedef GoogleClusterBuilder = FutureOr<BitmapDescriptor> Function(
|
||||
BuildContext context, List<DataPoint> dataPoints);
|
||||
|
||||
class GoogleInteractiveMap extends StatefulWidget {
|
||||
const GoogleInteractiveMap({
|
||||
super.key,
|
||||
this.initialPosition,
|
||||
this.initialZoom,
|
||||
this.dataPoints,
|
||||
this.clusterBuilder,
|
||||
this.onClusterTap,
|
||||
this.contentPadding,
|
||||
this.onMapCreated,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _GoogleInteractiveMapState();
|
||||
|
||||
final MapCoord? initialPosition;
|
||||
final double? initialZoom;
|
||||
final List<DataPoint>? dataPoints;
|
||||
final GoogleClusterBuilder? clusterBuilder;
|
||||
final void Function(List<DataPoint> dataPoints)? onClusterTap;
|
||||
final EdgeInsets? contentPadding;
|
||||
final void Function(InteractiveMapController controller)? onMapCreated;
|
||||
}
|
||||
|
||||
class _GoogleInteractiveMapState extends State<GoogleInteractiveMap> {
|
||||
@override
|
||||
void didUpdateWidget(covariant GoogleInteractiveMap oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.dataPoints != oldWidget.dataPoints) {
|
||||
_clusterManager.setItems(widget.dataPoints
|
||||
?.map((e) => _GoogleDataPoint(original: e))
|
||||
.toList() ??
|
||||
[]);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GoogleMap(
|
||||
mapType: MapType.normal,
|
||||
initialCameraPosition: widget.initialPosition?.let((e) => CameraPosition(
|
||||
target: widget.initialPosition!.toLatLng(),
|
||||
zoom: widget.initialZoom ?? 10,
|
||||
)) ??
|
||||
const CameraPosition(target: LatLng(0, 0)),
|
||||
markers: _markers,
|
||||
onMapCreated: _onMapCreated,
|
||||
onCameraMove: _clusterManager.onCameraMove,
|
||||
onCameraIdle: _clusterManager.updateMap,
|
||||
padding: widget.contentPadding ?? EdgeInsets.zero,
|
||||
);
|
||||
}
|
||||
|
||||
void _onMapCreated(GoogleMapController controller) {
|
||||
_parentController = _ParentController(controller);
|
||||
widget.onMapCreated?.call(_parentController!);
|
||||
|
||||
_clusterManager.setMapId(controller.mapId);
|
||||
|
||||
if (Theme.of(context).brightness == Brightness.dark) {
|
||||
controller.setMapStyle(_mapStyleNight);
|
||||
}
|
||||
}
|
||||
|
||||
late final _clusterManager = ClusterManager<_GoogleDataPoint>(
|
||||
const [],
|
||||
(markers) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_markers = markers;
|
||||
});
|
||||
}
|
||||
},
|
||||
markerBuilder: (cluster) async {
|
||||
final dataPoints = cluster.items.map((e) => e.original).toList();
|
||||
return Marker(
|
||||
markerId: MarkerId(cluster.getId()),
|
||||
position: cluster.location,
|
||||
onTap: () {
|
||||
widget.onClusterTap?.call(dataPoints);
|
||||
},
|
||||
icon: await widget.clusterBuilder?.call(context, dataPoints) ??
|
||||
BitmapDescriptor.defaultMarker,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
_ParentController? _parentController;
|
||||
var _markers = <Marker>{};
|
||||
}
|
||||
|
||||
class _GoogleDataPoint implements ClusterItem {
|
||||
const _GoogleDataPoint({
|
||||
required this.original,
|
||||
});
|
||||
|
||||
@override
|
||||
LatLng get location => original.position.toLatLng();
|
||||
|
||||
@override
|
||||
String get geohash =>
|
||||
Geohash.encode(location, codeLength: ClusterManager.precision);
|
||||
|
||||
final DataPoint original;
|
||||
}
|
||||
|
||||
class _ParentController implements InteractiveMapController {
|
||||
const _ParentController(this.controller);
|
||||
|
||||
@override
|
||||
void setPosition(MapCoord position) {
|
||||
controller
|
||||
.animateCamera(CameraUpdate.newLatLngZoom(position.toLatLng(), 10));
|
||||
}
|
||||
|
||||
final GoogleMapController controller;
|
||||
}
|
||||
|
||||
extension on MapCoord {
|
||||
LatLng toLatLng() => LatLng(latitude, longitude);
|
||||
}
|
||||
|
||||
// Generated by https://mapstyle.withgoogle.com/
|
||||
const _mapStyleNight =
|
||||
'[{"elementType":"geometry","stylers":[{"color":"#242f3e"}]},{"elementType":"labels.text.fill","stylers":[{"color":"#746855"}]},{"elementType":"labels.text.stroke","stylers":[{"color":"#242f3e"}]},{"featureType":"administrative.locality","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#263c3f"}]},{"featureType":"poi.park","elementType":"labels.text.fill","stylers":[{"color":"#6b9a76"}]},{"featureType":"road","elementType":"geometry","stylers":[{"color":"#38414e"}]},{"featureType":"road","elementType":"geometry.stroke","stylers":[{"color":"#212a37"}]},{"featureType":"road","elementType":"labels.text.fill","stylers":[{"color":"#9ca5b3"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#746855"}]},{"featureType":"road.highway","elementType":"geometry.stroke","stylers":[{"color":"#1f2835"}]},{"featureType":"road.highway","elementType":"labels.text.fill","stylers":[{"color":"#f3d19c"}]},{"featureType":"transit","elementType":"geometry","stylers":[{"color":"#2f3948"}]},{"featureType":"transit.station","elementType":"labels.text.fill","stylers":[{"color":"#d59563"}]},{"featureType":"water","elementType":"geometry","stylers":[{"color":"#17263c"}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"color":"#515c6d"}]},{"featureType":"water","elementType":"labels.text.stroke","stylers":[{"color":"#17263c"}]}]';
|
12
np_gps_map/lib/src/util.dart
Normal file
12
np_gps_map/lib/src/util.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
import 'package:np_gps_map/src/native.dart';
|
||||
import 'package:np_platform_util/np_platform_util.dart';
|
||||
|
||||
void initGpsMap() {
|
||||
if (getRawPlatform() == NpPlatform.android) {
|
||||
Native.isNewGMapsRenderer().then((value) => _isNewGMapsRenderer = value);
|
||||
}
|
||||
}
|
||||
|
||||
bool isNewGMapsRenderer() => _isNewGMapsRenderer;
|
||||
|
||||
var _isNewGMapsRenderer = false;
|
|
@ -12,14 +12,21 @@ dependencies:
|
|||
flutter:
|
||||
sdk: flutter
|
||||
flutter_map: ^6.1.0
|
||||
google_maps_flutter: ^2.2.8
|
||||
google_maps_flutter: 2.5.3
|
||||
google_maps_cluster_manager: 3.1.0
|
||||
latlong2: any
|
||||
np_async:
|
||||
path: ../np_async
|
||||
np_common:
|
||||
path: ../np_common
|
||||
np_platform_util:
|
||||
path: ../np_platform_util
|
||||
url_launcher: ^6.1.11
|
||||
|
||||
dependency_overrides:
|
||||
google_maps_flutter_android: 2.7.0
|
||||
google_maps_flutter_platform_interface: 2.5.0
|
||||
|
||||
dev_dependencies:
|
||||
np_lints:
|
||||
path: ../np_lints
|
||||
|
|
Loading…
Reference in a new issue