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);
|
await _initDiContainer(isolateType);
|
||||||
_initVisibilityDetector();
|
_initVisibilityDetector();
|
||||||
GpsMap.init();
|
initGpsMap();
|
||||||
// init session storage
|
// init session storage
|
||||||
SessionStorage();
|
SessionStorage();
|
||||||
|
|
||||||
|
|
|
@ -161,10 +161,12 @@ class PrefController {
|
||||||
value: value,
|
value: value,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<bool> setMapBrowserPrevPosition(MapCoord value) => _set<MapCoord?>(
|
Future<bool> setMapBrowserPrevPosition(MapCoord? value) =>
|
||||||
|
_setOrRemove<MapCoord>(
|
||||||
controller: _mapBrowserPrevPositionController,
|
controller: _mapBrowserPrevPositionController,
|
||||||
setter: (pref, value) => pref.setMapBrowserPrevPosition(
|
setter: (pref, value) => pref.setMapBrowserPrevPosition(
|
||||||
jsonEncode([value!.latitude, value.longitude])),
|
jsonEncode([value.latitude, value.longitude])),
|
||||||
|
remover: (pref) => pref.setMapBrowserPrevPosition(null),
|
||||||
value: value,
|
value: value,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -270,9 +272,9 @@ class PrefController {
|
||||||
BehaviorSubject.seeded(_c.pref.isDontShowVideoPreviewHintOr(false));
|
BehaviorSubject.seeded(_c.pref.isDontShowVideoPreviewHintOr(false));
|
||||||
@npSubjectAccessor
|
@npSubjectAccessor
|
||||||
late final _mapBrowserPrevPositionController = BehaviorSubject.seeded(_c.pref
|
late final _mapBrowserPrevPositionController = BehaviorSubject.seeded(_c.pref
|
||||||
.getMapBrowserPrevPosition()
|
.getMapBrowserPrevPosition()
|
||||||
?.let(tryJsonDecode)
|
?.let(tryJsonDecode)
|
||||||
?.let(_tryMapCoordFromJson));
|
?.let(_tryMapCoordFromJson));
|
||||||
}
|
}
|
||||||
|
|
||||||
@npSubjectAccessor
|
@npSubjectAccessor
|
||||||
|
|
|
@ -91,8 +91,13 @@ extension on Pref {
|
||||||
|
|
||||||
String? getMapBrowserPrevPosition() =>
|
String? getMapBrowserPrevPosition() =>
|
||||||
provider.getString(PrefKey.mapBrowserPrevPosition);
|
provider.getString(PrefKey.mapBrowserPrevPosition);
|
||||||
Future<bool> setMapBrowserPrevPosition(String value) =>
|
Future<bool> setMapBrowserPrevPosition(String? value) {
|
||||||
provider.setString(PrefKey.mapBrowserPrevPosition, value);
|
if (value == null) {
|
||||||
|
return provider.remove(PrefKey.mapBrowserPrevPosition);
|
||||||
|
} else {
|
||||||
|
return provider.setString(PrefKey.mapBrowserPrevPosition, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MapCoord? _tryMapCoordFromJson(dynamic json) {
|
MapCoord? _tryMapCoordFromJson(dynamic json) {
|
||||||
|
|
|
@ -28,4 +28,10 @@ extension StreamExtension<T> on Stream<T> {
|
||||||
|
|
||||||
Stream<T> distinctBy<U>(U Function(T element) keyOf) =>
|
Stream<T> distinctBy<U>(U Function(T element) keyOf) =>
|
||||||
distinct((previous, next) => keyOf(previous) == keyOf(next));
|
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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'package:intl/intl.dart' as intl;
|
import 'package:intl/intl.dart' as intl;
|
||||||
import 'package:kiwi/kiwi.dart';
|
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/k.dart' as k;
|
||||||
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/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/theme/dimension.dart';
|
import 'package:nc_photos/theme/dimension.dart';
|
||||||
import 'package:nc_photos/widget/collection_browser.dart';
|
import 'package:nc_photos/widget/collection_browser.dart';
|
||||||
|
|
|
@ -16,7 +16,6 @@ abstract class $_StateCopyWithWorker {
|
||||||
_State call(
|
_State call(
|
||||||
{List<_DataPoint>? data,
|
{List<_DataPoint>? data,
|
||||||
MapCoord? initialPoint,
|
MapCoord? initialPoint,
|
||||||
Set<Marker>? markers,
|
|
||||||
bool? isShowDataRangeControlPanel,
|
bool? isShowDataRangeControlPanel,
|
||||||
_DateRangeType? dateRangeType,
|
_DateRangeType? dateRangeType,
|
||||||
DateRange? localDateRange,
|
DateRange? localDateRange,
|
||||||
|
@ -30,7 +29,6 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||||
_State call(
|
_State call(
|
||||||
{dynamic data,
|
{dynamic data,
|
||||||
dynamic initialPoint = copyWithNull,
|
dynamic initialPoint = copyWithNull,
|
||||||
dynamic markers,
|
|
||||||
dynamic isShowDataRangeControlPanel,
|
dynamic isShowDataRangeControlPanel,
|
||||||
dynamic dateRangeType,
|
dynamic dateRangeType,
|
||||||
dynamic localDateRange,
|
dynamic localDateRange,
|
||||||
|
@ -40,7 +38,6 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||||
initialPoint: initialPoint == copyWithNull
|
initialPoint: initialPoint == copyWithNull
|
||||||
? that.initialPoint
|
? that.initialPoint
|
||||||
: initialPoint as MapCoord?,
|
: initialPoint as MapCoord?,
|
||||||
markers: markers as Set<Marker>? ?? that.markers,
|
|
||||||
isShowDataRangeControlPanel: isShowDataRangeControlPanel as bool? ??
|
isShowDataRangeControlPanel: isShowDataRangeControlPanel as bool? ??
|
||||||
that.isShowDataRangeControlPanel,
|
that.isShowDataRangeControlPanel,
|
||||||
dateRangeType: dateRangeType as _DateRangeType? ?? that.dateRangeType,
|
dateRangeType: dateRangeType as _DateRangeType? ?? that.dateRangeType,
|
||||||
|
@ -74,7 +71,7 @@ extension _$_BlocNpLog on _Bloc {
|
||||||
extension _$_StateToString on _State {
|
extension _$_StateToString on _State {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// 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 {
|
extension _$_OpenDataRangeControlPanelToString on _OpenDataRangeControlPanel {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// ignore: unnecessary_string_interpolations
|
||||||
|
|
|
@ -13,15 +13,15 @@ class _Bloc extends Bloc<_Event, _State>
|
||||||
_calcDateRange(clock.now().toDate(), _DateRangeType.thisMonth),
|
_calcDateRange(clock.now().toDate(), _DateRangeType.thisMonth),
|
||||||
)) {
|
)) {
|
||||||
on<_LoadData>(_onLoadData);
|
on<_LoadData>(_onLoadData);
|
||||||
on<_SetMarkers>(_onSetMarkers);
|
|
||||||
on<_OpenDataRangeControlPanel>(_onOpenDataRangeControlPanel);
|
on<_OpenDataRangeControlPanel>(_onOpenDataRangeControlPanel);
|
||||||
on<_CloseControlPanel>(_onCloseControlPanel);
|
on<_CloseControlPanel>(_onCloseControlPanel);
|
||||||
on<_SetDateRangeType>(_onSetDateRangeType);
|
on<_SetDateRangeType>(_onSetDateRangeType);
|
||||||
on<_SetLocalDateRange>(_onSetDateRange);
|
on<_SetLocalDateRange>(_onSetDateRange);
|
||||||
on<_SetError>(_onSetError);
|
on<_SetError>(_onSetError);
|
||||||
|
|
||||||
_subscriptions
|
_subscriptions.add(stream
|
||||||
.add(stream.distinctBy((state) => state.localDateRange).listen((state) {
|
.distinctByIgnoreFirst((state) => state.localDateRange)
|
||||||
|
.listen((state) {
|
||||||
add(const _LoadData());
|
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(
|
void _onOpenDataRangeControlPanel(
|
||||||
_OpenDataRangeControlPanel ev, Emitter<_State> emit) {
|
_OpenDataRangeControlPanel ev, Emitter<_State> emit) {
|
||||||
_log.info(ev);
|
_log.info(ev);
|
||||||
|
|
|
@ -6,7 +6,6 @@ class _State {
|
||||||
const _State({
|
const _State({
|
||||||
required this.data,
|
required this.data,
|
||||||
this.initialPoint,
|
this.initialPoint,
|
||||||
required this.markers,
|
|
||||||
required this.isShowDataRangeControlPanel,
|
required this.isShowDataRangeControlPanel,
|
||||||
required this.dateRangeType,
|
required this.dateRangeType,
|
||||||
required this.localDateRange,
|
required this.localDateRange,
|
||||||
|
@ -19,7 +18,6 @@ class _State {
|
||||||
}) {
|
}) {
|
||||||
return _State(
|
return _State(
|
||||||
data: const [],
|
data: const [],
|
||||||
markers: const {},
|
|
||||||
isShowDataRangeControlPanel: false,
|
isShowDataRangeControlPanel: false,
|
||||||
dateRangeType: dateRangeType,
|
dateRangeType: dateRangeType,
|
||||||
localDateRange: localDateRange,
|
localDateRange: localDateRange,
|
||||||
|
@ -31,7 +29,6 @@ class _State {
|
||||||
|
|
||||||
final List<_DataPoint> data;
|
final List<_DataPoint> data;
|
||||||
final MapCoord? initialPoint;
|
final MapCoord? initialPoint;
|
||||||
final Set<Marker> markers;
|
|
||||||
|
|
||||||
final bool isShowDataRangeControlPanel;
|
final bool isShowDataRangeControlPanel;
|
||||||
final _DateRangeType dateRangeType;
|
final _DateRangeType dateRangeType;
|
||||||
|
@ -52,16 +49,6 @@ class _LoadData implements _Event {
|
||||||
String toString() => _$toString();
|
String toString() => _$toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@toString
|
|
||||||
class _SetMarkers implements _Event {
|
|
||||||
const _SetMarkers(this.markers);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => _$toString();
|
|
||||||
|
|
||||||
final Set<Marker> markers;
|
|
||||||
}
|
|
||||||
|
|
||||||
@toString
|
@toString
|
||||||
class _OpenDataRangeControlPanel implements _Event {
|
class _OpenDataRangeControlPanel implements _Event {
|
||||||
const _OpenDataRangeControlPanel();
|
const _OpenDataRangeControlPanel();
|
||||||
|
|
|
@ -1,25 +1,175 @@
|
||||||
part of '../map_browser.dart';
|
part of '../map_browser.dart';
|
||||||
|
|
||||||
class _DataPoint implements ClusterItem {
|
class _DataPoint extends DataPoint {
|
||||||
const _DataPoint({
|
const _DataPoint({
|
||||||
required this.location,
|
required super.position,
|
||||||
required this.fileId,
|
required this.fileId,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory _DataPoint.fromImageLatLng(ImageLatLng src) => _DataPoint(
|
factory _DataPoint.fromImageLatLng(ImageLatLng src) => _DataPoint(
|
||||||
location: LatLng(src.latitude, src.longitude),
|
position: MapCoord(src.latitude, src.longitude),
|
||||||
fileId: src.fileId,
|
fileId: src.fileId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
|
||||||
String get geohash =>
|
|
||||||
Geohash.encode(location, codeLength: ClusterManager.precision);
|
|
||||||
|
|
||||||
@override
|
|
||||||
final LatLng location;
|
|
||||||
final int fileId;
|
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 {
|
enum _DateRangeType {
|
||||||
thisMonth,
|
thisMonth,
|
||||||
prevMonth,
|
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) {
|
Widget build(BuildContext context) {
|
||||||
return MultiBlocListener(
|
return MultiBlocListener(
|
||||||
listeners: [
|
listeners: [
|
||||||
_BlocListenerT<List<_DataPoint>>(
|
|
||||||
selector: (state) => state.data,
|
|
||||||
listener: (context, data) {
|
|
||||||
_clusterManager.setItems(data);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_BlocListenerT<MapCoord?>(
|
_BlocListenerT<MapCoord?>(
|
||||||
selector: (state) => state.initialPoint,
|
selector: (state) => state.initialPoint,
|
||||||
listener: (context, initialPoint) {
|
listener: (context, initialPoint) {
|
||||||
if (initialPoint != null) {
|
if (initialPoint != null) {
|
||||||
_mapController?.animateCamera(
|
_controller?.setPosition(initialPoint);
|
||||||
CameraUpdate.newLatLngZoom(initialPoint.toLatLng(), 10));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: _BlocBuilder(
|
child: _BlocBuilder(
|
||||||
buildWhen: (previous, current) => previous.markers != current.markers,
|
buildWhen: (previous, current) => previous.data != current.data,
|
||||||
builder: (context, state) => GoogleMap(
|
builder: (context, state) {
|
||||||
mapType: MapType.normal,
|
final prevPosition =
|
||||||
initialCameraPosition: context
|
context.read<PrefController>().mapBrowserPrevPositionValue;
|
||||||
.read<PrefController>()
|
return ValueStreamBuilder<GpsMapProvider>(
|
||||||
.mapBrowserPrevPositionValue
|
stream: context.bloc.prefController.gpsMapProvider,
|
||||||
?.let(
|
builder: (context, gpsMapProvider) => InteractiveMap(
|
||||||
(p) => CameraPosition(target: p.toLatLng(), zoom: 10)) ??
|
providerHint: gpsMapProvider.requireData,
|
||||||
const CameraPosition(target: LatLng(0, 0)),
|
initialPosition: prevPosition ?? const MapCoord(0, 0),
|
||||||
markers: state.markers,
|
initialZoom: prevPosition == null ? 2.5 : 10,
|
||||||
onMapCreated: (controller) {
|
dataPoints: state.data,
|
||||||
_clusterManager.setMapId(controller.mapId);
|
onClusterTap: (dataPoints) {
|
||||||
_mapController = controller;
|
final c = Collection(
|
||||||
if (Theme.of(context).brightness == Brightness.dark) {
|
name: "",
|
||||||
controller.setMapStyle(_mapStyleNight);
|
contentProvider: CollectionAdHocProvider(
|
||||||
}
|
account: context.bloc.account,
|
||||||
if (state.initialPoint != null) {
|
fileIds: dataPoints
|
||||||
controller.animateCamera(CameraUpdate.newLatLngZoom(
|
.cast<_DataPoint>()
|
||||||
state.initialPoint!.toLatLng(), 10));
|
.map((e) => e.fileId)
|
||||||
}
|
.toList(),
|
||||||
},
|
),
|
||||||
onCameraMove: _clusterManager.onCameraMove,
|
);
|
||||||
onCameraIdle: _clusterManager.updateMap,
|
Navigator.of(context).pushNamed(
|
||||||
padding: EdgeInsets.only(
|
CollectionBrowser.routeName,
|
||||||
top: MediaQuery.of(context).padding.top,
|
arguments: CollectionBrowserArguments(c),
|
||||||
bottom: MediaQuery.of(context).padding.bottom,
|
);
|
||||||
|
},
|
||||||
|
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(
|
final double size;
|
||||||
int size, {
|
final String text;
|
||||||
String? text,
|
final double textSize;
|
||||||
required Color color,
|
final 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PanelContainer extends StatefulWidget {
|
class _PanelContainer extends StatefulWidget {
|
||||||
|
@ -430,7 +331,7 @@ class _DateFieldState extends State<_DateField> {
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final result = await showDatePicker(
|
final result = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
firstDate: DateTime(1970),
|
firstDate: DateTime.fromMillisecondsSinceEpoch(0),
|
||||||
lastDate: clock.now(),
|
lastDate: clock.now(),
|
||||||
currentDate: widget.date.toLocalDateTime(),
|
currentDate: widget.date.toLocalDateTime(),
|
||||||
);
|
);
|
||||||
|
@ -458,7 +359,3 @@ class _DateFieldState extends State<_DateField> {
|
||||||
|
|
||||||
late final _controller = TextEditingController(text: _stringify(widget.date));
|
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"
|
url: "https://gitlab.com/nc-photos/plus_plugins"
|
||||||
source: git
|
source: git
|
||||||
version: "3.1.1"
|
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:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -545,6 +553,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
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:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -661,7 +685,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.0"
|
version: "6.3.0"
|
||||||
google_maps_cluster_manager:
|
google_maps_cluster_manager:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: google_maps_cluster_manager
|
name: google_maps_cluster_manager
|
||||||
sha256: "36e9a4b2d831c470fc85d692a6c9cec70e0f385d578b9697de5f4de347561b83"
|
sha256: "36e9a4b2d831c470fc85d692a6c9cec70e0f385d578b9697de5f4de347561b83"
|
||||||
|
|
|
@ -159,7 +159,6 @@ dependencies:
|
||||||
woozy_search: ^2.0.3
|
woozy_search: ^2.0.3
|
||||||
|
|
||||||
google_maps_flutter: 2.5.3
|
google_maps_flutter: 2.5.3
|
||||||
google_maps_cluster_manager: 3.1.0
|
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
video_player:
|
video_player:
|
||||||
|
@ -177,6 +176,7 @@ dependency_overrides:
|
||||||
url: https://gitlab.com/nc-photos/flutter-plugins
|
url: https://gitlab.com/nc-photos/flutter-plugins
|
||||||
ref: video_player-v2.8.6-nc-photos-2
|
ref: video_player-v2.8.6-nc-photos-2
|
||||||
path: packages/video_player/video_player_platform_interface
|
path: packages/video_player/video_player_platform_interface
|
||||||
|
# fix google_maps_cluster_manager
|
||||||
google_maps_flutter_android: 2.7.0
|
google_maps_flutter_android: 2.7.0
|
||||||
google_maps_flutter_platform_interface: 2.5.0
|
google_maps_flutter_platform_interface: 2.5.0
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
library np_gps_map;
|
library np_gps_map;
|
||||||
|
|
||||||
export 'src/gps_map.dart';
|
export 'src/gps_map.dart';
|
||||||
|
export 'src/interactive_map.dart';
|
||||||
export 'src/map_coord.dart';
|
export 'src/map_coord.dart';
|
||||||
|
export 'src/util.dart' show initGpsMap;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:np_gps_map/src/map_coord.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'
|
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';
|
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/osm_gps_map.dart';
|
||||||
|
import 'package:np_gps_map/src/util.dart';
|
||||||
import 'package:np_platform_util/np_platform_util.dart';
|
import 'package:np_platform_util/np_platform_util.dart';
|
||||||
|
|
||||||
enum GpsMapProvider {
|
enum GpsMapProvider {
|
||||||
|
@ -21,16 +21,10 @@ class GpsMap extends StatelessWidget {
|
||||||
this.onTap,
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
static void init() {
|
|
||||||
if (getRawPlatform() == NpPlatform.android) {
|
|
||||||
Native.isNewGMapsRenderer().then((value) => _isNewGMapsRenderer = value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (providerHint == GpsMapProvider.osm ||
|
if (providerHint == GpsMapProvider.osm ||
|
||||||
(getRawPlatform() == NpPlatform.android && !_isNewGMapsRenderer)) {
|
(getRawPlatform() == NpPlatform.android && !isNewGMapsRenderer())) {
|
||||||
return OsmGpsMap(
|
return OsmGpsMap(
|
||||||
center: center,
|
center: center,
|
||||||
zoom: zoom,
|
zoom: zoom,
|
||||||
|
@ -53,6 +47,4 @@ class GpsMap extends StatelessWidget {
|
||||||
final MapCoord center;
|
final MapCoord center;
|
||||||
final double zoom;
|
final double zoom;
|
||||||
final void Function()? onTap;
|
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:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_map: ^6.1.0
|
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
|
latlong2: any
|
||||||
np_async:
|
np_async:
|
||||||
path: ../np_async
|
path: ../np_async
|
||||||
|
np_common:
|
||||||
|
path: ../np_common
|
||||||
np_platform_util:
|
np_platform_util:
|
||||||
path: ../np_platform_util
|
path: ../np_platform_util
|
||||||
url_launcher: ^6.1.11
|
url_launcher: ^6.1.11
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
google_maps_flutter_android: 2.7.0
|
||||||
|
google_maps_flutter_platform_interface: 2.5.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
np_lints:
|
np_lints:
|
||||||
path: ../np_lints
|
path: ../np_lints
|
||||||
|
|
Loading…
Reference in a new issue