mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
Merge branch 'map-browser-osm'
This commit is contained in:
commit
080f89caca
19 changed files with 670 additions and 263 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';
|
||||
|
@ -24,6 +23,7 @@ import 'package:nc_photos/exception_event.dart';
|
|||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/stream_extension.dart';
|
||||
import 'package:nc_photos/stream_util.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/theme/dimension.dart';
|
||||
import 'package:nc_photos/widget/collection_browser.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,175 @@
|
|||
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 _MarkerBuilder {
|
||||
_MarkerBuilder(this.context);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
double _getMarkerRatio(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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Widget build(List<DataPoint> dataPoints) {
|
||||
final text = _getMarkerCountString(dataPoints.length);
|
||||
return _OsmMarker(
|
||||
size: _getMarkerSize(dataPoints.length),
|
||||
text: text,
|
||||
textSize: _getMarkerTextSize(text, dataPoints.length),
|
||||
color: _getMarkerColor(dataPoints.length),
|
||||
);
|
||||
}
|
||||
|
||||
double _getMarkerSize(int count) {
|
||||
final r = _getMarkerRatio(count);
|
||||
return (r * 28).toInt() + 28;
|
||||
}
|
||||
|
||||
double _getMarkerTextSize(String text, int count) {
|
||||
final r = _getMarkerRatio(count);
|
||||
return (r * 3) + 9 - ((text.length / 6) * 1);
|
||||
}
|
||||
|
||||
Color _getMarkerColor(int count) {
|
||||
final r = _getMarkerRatio(count);
|
||||
return HSLColor.lerp(_minColorHsl, _maxColorHsl, r)!.toColor();
|
||||
}
|
||||
}
|
||||
|
||||
class _GoogleMarkerBuilder extends _MarkerBuilder {
|
||||
_GoogleMarkerBuilder(super.context);
|
||||
|
||||
Future<BitmapDescriptor> build(List<DataPoint> dataPoints) {
|
||||
return _getClusterBitmap(
|
||||
_getMarkerSize(dataPoints.length),
|
||||
text: _getMarkerCountString(dataPoints.length),
|
||||
color: _getMarkerColor(dataPoints.length),
|
||||
);
|
||||
}
|
||||
|
||||
double _getMarkerSize(int count) {
|
||||
final r = _getMarkerRatio(count);
|
||||
return (r * 75).toInt() + 100;
|
||||
}
|
||||
|
||||
Color _getMarkerColor(int count) {
|
||||
final r = _getMarkerRatio(count);
|
||||
return HSLColor.lerp(_minColorHsl, _maxColorHsl, r)!.toColor();
|
||||
}
|
||||
|
||||
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 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());
|
||||
}
|
||||
}
|
||||
|
||||
enum _DateRangeType {
|
||||
thisMonth,
|
||||
prevMonth,
|
||||
|
@ -40,7 +190,3 @@ enum _DateRangeType {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on MapCoord {
|
||||
LatLng toLatLng() => LatLng(latitude, longitude);
|
||||
}
|
||||
|
|
|
@ -12,212 +12,113 @@ 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 ValueStreamBuilder<GpsMapProvider>(
|
||||
stream: context.bloc.prefController.gpsMapProvider,
|
||||
builder: (context, gpsMapProvider) => InteractiveMap(
|
||||
providerHint: gpsMapProvider.requireData,
|
||||
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),
|
||||
osmClusterBuilder: (context, dataPoints) =>
|
||||
_OsmMarkerBuilder(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!);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InteractiveMapController? _controller;
|
||||
}
|
||||
|
||||
class _OsmMarker extends StatelessWidget {
|
||||
const _OsmMarker({
|
||||
required this.size,
|
||||
required this.text,
|
||||
required this.textSize,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(size / 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(.3),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(1, 1),
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(.75),
|
||||
width: 1.5,
|
||||
),
|
||||
color: color,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: textSize,
|
||||
color: Colors.white.withOpacity(.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
final double size;
|
||||
final String text;
|
||||
final double textSize;
|
||||
final Color color;
|
||||
}
|
||||
|
||||
class _PanelContainer extends StatefulWidget {
|
||||
|
@ -430,7 +331,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 +359,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"}]}]';
|
||||
|
|
|
@ -26,6 +26,14 @@ packages:
|
|||
url: "https://gitlab.com/nc-photos/plus_plugins"
|
||||
source: git
|
||||
version: "3.1.1"
|
||||
animated_stack_widget:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: animated_stack_widget
|
||||
sha256: ce4788dd158768c9d4388354b6fb72600b78e041a37afc4c279c63ecafcb9408
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.4"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -545,6 +553,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
flutter_map_marker_cluster:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_map_marker_cluster
|
||||
sha256: a324f48da5ee83a3f29fd8d08b4b1e6e3114ff5c6cab910124d6a2e1f06f08cc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.6"
|
||||
flutter_map_marker_popup:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_map_marker_popup
|
||||
sha256: ec563bcbae24a18ac16815fb75ac5ab33ccba609e14db70e252a67de19c6639c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -661,7 +685,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;
|
||||
}
|
||||
|
|
73
np_gps_map/lib/src/interactive_map.dart
Normal file
73
np_gps_map/lib/src/interactive_map.dart
Normal file
|
@ -0,0 +1,73 @@
|
|||
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/interactive_map/osm.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.osmClusterBuilder,
|
||||
this.googleClusterBuilder,
|
||||
this.contentPadding,
|
||||
this.onMapCreated,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (providerHint == GpsMapProvider.osm ||
|
||||
(getRawPlatform() == NpPlatform.android && !isNewGMapsRenderer())) {
|
||||
return OsmInteractiveMap(
|
||||
initialPosition: initialPosition,
|
||||
initialZoom: initialZoom,
|
||||
dataPoints: dataPoints,
|
||||
onClusterTap: onClusterTap,
|
||||
clusterBuilder: osmClusterBuilder,
|
||||
contentPadding: contentPadding,
|
||||
onMapCreated: onMapCreated,
|
||||
);
|
||||
} 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 OsmClusterBuilder? osmClusterBuilder;
|
||||
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"}]}]';
|
131
np_gps_map/lib/src/interactive_map/osm.dart
Normal file
131
np_gps_map/lib/src/interactive_map/osm.dart
Normal file
|
@ -0,0 +1,131 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
|
||||
import 'package:latlong2/latlong.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 OsmClusterBuilder = Widget Function(
|
||||
BuildContext context, List<DataPoint> dataPoints);
|
||||
|
||||
class OsmInteractiveMap extends StatefulWidget {
|
||||
const OsmInteractiveMap({
|
||||
super.key,
|
||||
this.initialPosition,
|
||||
this.initialZoom,
|
||||
this.dataPoints,
|
||||
this.clusterBuilder,
|
||||
this.onClusterTap,
|
||||
this.contentPadding,
|
||||
this.onMapCreated,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _OsmInteractiveMapState();
|
||||
|
||||
final MapCoord? initialPosition;
|
||||
final double? initialZoom;
|
||||
final List<DataPoint>? dataPoints;
|
||||
final OsmClusterBuilder? clusterBuilder;
|
||||
final void Function(List<DataPoint> dataPoints)? onClusterTap;
|
||||
final EdgeInsets? contentPadding;
|
||||
final void Function(InteractiveMapController controller)? onMapCreated;
|
||||
}
|
||||
|
||||
class _OsmInteractiveMapState extends State<OsmInteractiveMap> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_parentController == null) {
|
||||
_parentController = _ParentController(_controller);
|
||||
widget.onMapCreated?.call(_parentController!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlutterMap(
|
||||
mapController: _controller,
|
||||
options: MapOptions(
|
||||
initialCenter: widget.initialPosition?.toLatLng() ?? const LatLng(0, 0),
|
||||
initialZoom: max(2.5, widget.initialZoom ?? 2.5),
|
||||
minZoom: 2.5,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
),
|
||||
if (widget.dataPoints != null)
|
||||
MarkerClusterLayerWidget(
|
||||
options: MarkerClusterLayerOptions(
|
||||
markers: widget.dataPoints!
|
||||
.map((e) => _OsmDataPoint(
|
||||
original: e,
|
||||
child: _buildMarker(context, [e]),
|
||||
))
|
||||
.toList(),
|
||||
builder: (context, markers) => _buildMarker(
|
||||
context,
|
||||
markers.cast<_OsmDataPoint>().map((e) => e.original).toList(),
|
||||
),
|
||||
// need to be large enough to contain markers of all size
|
||||
size: const Size.square(120),
|
||||
// disable all tap handlers from package
|
||||
zoomToBoundsOnClick: false,
|
||||
centerMarkerOnClick: false,
|
||||
spiderfyCluster: false,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: widget.contentPadding ?? EdgeInsets.zero,
|
||||
child: const SimpleAttributionWidget(
|
||||
source: Text("OpenStreetMap contributors"),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMarker(BuildContext context, List<DataPoint> dataPoints) {
|
||||
if (widget.clusterBuilder == null) {
|
||||
return const SizedBox.shrink();
|
||||
} else {
|
||||
return GestureDetector(
|
||||
onTap: widget.onClusterTap?.let((l) => () => l(dataPoints)),
|
||||
child: widget.clusterBuilder!(context, dataPoints),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_ParentController? _parentController;
|
||||
late final _controller = MapController();
|
||||
}
|
||||
|
||||
class _OsmDataPoint extends Marker {
|
||||
_OsmDataPoint({
|
||||
required this.original,
|
||||
required super.child,
|
||||
}) : super(point: original.position.toLatLng());
|
||||
|
||||
final DataPoint original;
|
||||
}
|
||||
|
||||
class _ParentController implements InteractiveMapController {
|
||||
const _ParentController(this.controller);
|
||||
|
||||
@override
|
||||
void setPosition(MapCoord position) {
|
||||
controller.move(position.toLatLng(), 10);
|
||||
}
|
||||
|
||||
final MapController controller;
|
||||
}
|
||||
|
||||
extension on MapCoord {
|
||||
LatLng toLatLng() => LatLng(latitude, longitude);
|
||||
}
|
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,22 @@ dependencies:
|
|||
flutter:
|
||||
sdk: flutter
|
||||
flutter_map: ^6.1.0
|
||||
google_maps_flutter: ^2.2.8
|
||||
flutter_map_marker_cluster: ^1.3.6
|
||||
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