diff --git a/lib/main.dart b/lib/main.dart index c1385b2..51506ad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -69,7 +69,7 @@ void main() async { var isTrackingLocation = await AppSettings.instance.trackingLocation.get(); log("isTrackingLocation: $isTrackingLocation"); - if (isTrackingLocation!) { + if (isTrackingLocation == true) { await FlutterBackgroundService().startService(); // FlutterBackgroundService().invoke("startTracking"); } diff --git a/lib/src/application/background_tasks.dart b/lib/src/application/background_tasks.dart index b1f9c84..a2524dc 100644 --- a/lib/src/application/background_tasks.dart +++ b/lib/src/application/background_tasks.dart @@ -49,7 +49,7 @@ FutureOr onStart(ServiceInstance service) async { }); service.on("saveTrackingData").listen((event) async { var isTrackingLocation = await AppSettings.instance.trackingLocation.get(); - if (isTrackingLocation != null && isTrackingLocation) { + if (isTrackingLocation == true) { await locationService.saveToday(); } }); diff --git a/lib/src/application/homepoints.dart b/lib/src/application/homepoints.dart index c80b81f..13e2a57 100644 --- a/lib/src/application/homepoints.dart +++ b/lib/src/application/homepoints.dart @@ -15,6 +15,8 @@ class HomepointManager { late Directory _appDir; late File _homepointFile; + List>? visits; + HomepointManager() {} Map get homepoints { @@ -50,6 +52,40 @@ class HomepointManager { jsonMap.map((key, value) => MapEntry(key, Homepoint.fromJson(value))); } + List> getVisits(List dataPoints) { + Map visited = + Map.fromEntries(homepoints.entries.map((e) => MapEntry(e.key, 0))); + for (final h in homepoints.entries) { + final homeLatLng = h.value.position; + + var atHP = false; + var i = 0; + while (i < dataPoints.length) { + var dp = dataPoints[i]; + final dpPos = LatLng(dp.position.latitude, dp.position.longitude); + if (const Distance().distance(dpPos, homeLatLng) < h.value.radius) { + atHP = true; + } else if (atHP) { + // true when leaving HP + // dataPoint is within homepoint radius + visited.update(h.key, (value) => value + 1); + atHP = false; + } + i++; + } + if (atHP) { + // true when last index was atHP + // dataPoint is within homepoint radius + visited.update(h.key, (value) => value + 1); + atHP = false; + } + } + visits = visited.entries + .map((e) => MapEntry(homepoints[e.key]!.name, e.value)) + .toList(); + return visits!; + } + addPoint(Homepoint point) { _homepoints.putIfAbsent(_uuid.v4(), () => point); save(); diff --git a/lib/src/application/location.dart b/lib/src/application/location.dart index 08b7d47..9f07aff 100644 --- a/lib/src/application/location.dart +++ b/lib/src/application/location.dart @@ -82,6 +82,155 @@ class LocationService { return _initialized; } + int get stepsMin { + for (final p in dataPoints) { + if (p.steps != null) { + return p.steps!; + } + } + return 0; + } + + /// total steps + /// + /// from last measured steps count - first measured steps count + int get stepsTotal { + if (hasPositions) { + var stepsStart = stepsMin; + var stepsEnd = stepsMin; + for (final p in dataPoints.reversed) { + if (p.steps != null) { + stepsEnd = p.steps!; + break; + } + } + return stepsEnd - stepsStart; + } else { + return 0; + } + } + + List get hourlyStepsTotal { + List hours = List.generate(24, (index) => 0); + var stepsBefore = stepsMin; + for (final p in dataPoints) { + if (p.timestamp != null) { + var i = p.timestamp!.toLocal().hour; + if (p.steps != null && p.steps! > stepsBefore) { + hours[i] += p.steps! - stepsBefore; + stepsBefore = p.steps!; + } + } + } + return hours; + } + + /// total distance in meters + /// TODO: more accurate distance calculation + double get distanceTotal { + double dist = 0; + for (var i = 0; i < dataPoints.length - 1; i++) { + final p1 = dataPoints[i]; + final p2 = dataPoints[i + 1]; + dist += const Distance().distance( + LatLng(p1.latitude, p1.longitude), LatLng(p2.latitude, p2.longitude)); + } + return dist; + } + + List get hourlyDistanceTotal { + List hours = List.generate(24, (index) => 0); + for (var i = 1; i < dataPoints.length; i++) { + var beforeP = dataPoints[i - 1]; + var p = dataPoints[i]; + + if (p.timestamp != null && beforeP.timestamp != null) { + var i = p.timestamp!.toLocal().hour; + hours[i] += Distance().distance( + LatLng(beforeP.latitude, beforeP.longitude), + LatLng(p.latitude, p.longitude)); + } + } + return hours; + } + + LocationDataPoint dataPointClosestTo(DateTime time) { + int? smallestDiff; + int smallestDiffI = 0; + var timeMillis = time.dayInMillis(); + + var i = 0; + while (i < dataPoints.length) { + final p = dataPoints[i]; + if (p.timestamp != null) { + var pMillis = p.timestamp!.dayInMillis(); + final diff = (timeMillis - pMillis).abs(); + + if (smallestDiff == null) { + smallestDiff = diff; + smallestDiffI = 0; + } + + log("$i - $diff"); + if (smallestDiff != null && diff < smallestDiff!) { + log("index: $i"); + smallestDiffI = i; + smallestDiff = diff; + } + i++; + } else { + i++; + } + } + log("--- index $smallestDiffI ---"); + return dataPoints[smallestDiffI]; + } + + /// list of closest datapoint for every minute of the day + /// + /// dataPointPerMinute.length = 24 * 60 = 1440 + List get dataPointPerMinute { + List minutesNullable = + List.generate(1440, (index) => null); + for (var i = 0; i < dataPoints.length; i++) { + var beforeP = i > 0 ? dataPoints[i - 1] : dataPoints[i]; + var p = dataPoints[i]; + + final pMinute = p.timestamp!.dayInMinutes(); + if (minutesNullable[pMinute] == null) { + minutesNullable[pMinute] = p.clone(); + if (minutesNullable[pMinute]!.steps != null) { + minutesNullable[pMinute]!.steps = minutesNullable[pMinute]!.steps! - + (minutesNullable[pMinute]!.steps ?? 0); + } + } else { + // add up miutes of datapoints in the same minute + if (minutesNullable[pMinute]!.steps != null) { + minutesNullable[pMinute]!.steps = + (minutesNullable[pMinute]!.steps ?? 0) + + (p.steps != null ? (p.steps! - (beforeP.steps ?? 0)) : 0); + } + // set pedStatus if its unknown on the current datapoint + if (minutesNullable[pMinute]!.pedStatus == + LocationDataPoint.STATUS_UNKNOWN && + p.pedStatus != LocationDataPoint.STATUS_UNKNOWN) { + minutesNullable[pMinute]!.pedStatus == p.pedStatus; + } + } + } + List minutes = []; + for (var i = 0; i < minutesNullable.length; i++) { + var p = minutesNullable[i]; + if (p == null) { + var closestI = minutesNullable.closestNonNull(i); + if (closestI != null) { + p = minutesNullable[closestI]; + } + } + minutes.add(p ?? dataPoints.first); + } + return minutes; + } Future record({Function(Position)? onReady}) async { log("start recording position data"); @@ -172,7 +321,6 @@ class LocationService { return false; } - void addPosition(Position position) { // LocationDataPoint can only have steps > 0 if ped status is not stopped // -> detecting stops clearer? @@ -180,8 +328,6 @@ class LocationService { dataPoints.add(LocationDataPoint(position, _newSteps, _newPedStatus)); } - - void optimizeCapturedData() { // remove redundant data points from positions list // - keep only start and end of a stop @@ -285,9 +431,6 @@ class LocationService { segmentedData = segments; } - - - List fromGPX(String xml, {bool setPos = true}) { var xmlGpx = GpxReader().fromString(xml); List posList = []; @@ -307,16 +450,14 @@ class LocationService { } if (ext["speed"] != null) speed = double.parse(ext["speed"] ?? "0"); if (ext["steps"] != null) { - steps = gpxDesc["steps"] != "null" - ? int.parse(gpxDesc["steps"] ?? "0") - : null; + steps = int.tryParse(ext["steps"]!); } posList.add(LocationDataPoint( Position( longitude: trkpt.lon!, latitude: trkpt.lat!, - timestamp: trkpt.time, + timestamp: trkpt.time?.toLocal(), accuracy: 0, altitude: trkpt.ele!, heading: heading, @@ -379,19 +520,19 @@ class LocationService { trksegs.add(Trkseg( trkpts: seg.dataPoints .map((p) => Wpt( - ele: p.altitude, - lat: p.latitude, - lon: p.longitude, - time: p.timestamp, - type: typeBasedOnSpeed(p.pedStatus, p.speed), - desc: - "steps:${p.steps};heading:${p.heading};speed:${p.speed}", - extensions: { - "pedStatus": p.pedStatus, - "steps": p.steps.toString(), - "heading": p.heading.toStringAsFixed(2), - "speed": p.speed.toStringAsFixed(2), - })) + ele: p.altitude, + lat: p.latitude, + lon: p.longitude, + time: p.timestamp, + type: typeBasedOnSpeed(p.pedStatus, p.speed), + desc: + "steps:${p.steps};heading:${p.heading};speed:${p.speed}", + extensions: { + "pedStatus": p.pedStatus, + "steps": p.steps.toString(), + "heading": p.heading.toStringAsFixed(2), + "speed": p.speed.toStringAsFixed(2), + })) .toList(), extensions: { "duration": "${seg.duration().inSeconds}s", @@ -448,9 +589,6 @@ class LocationService { log("gpx file exported to $gpxFilePath"); } - - - Future loadToday() async { String date = DateTime.now().toLocal().toIso8601String().split("T").first; var gpxDirPath = "${appDir.path}/gpxData"; @@ -474,9 +612,10 @@ class LocationService { // check if its a new day and if so, remove all data from previous day // necessary because a new gpx file is created for every day -> no overlay in data // dates are converted to utc, because gpx stores dates as utc -> gpx files will not start before 0:00 and not end after 23:59 - if (lastDate.day != now.day) { - dataPoints = - dataPoints.where((p) => p.timestamp!.day == now.day).toList(); + if (lastDate.toLocal().day != now.toLocal().day) { + dataPoints = dataPoints + .where((p) => p.timestamp!.toLocal().day == now.toLocal().day) + .toList(); lastDate = now; } String date = lastDate.toLocal().toIso8601String().split("T").first; @@ -502,7 +641,6 @@ class LocationService { dataPoints = []; } - StreamSubscription streamPosition(Function(Position) addPosition) { late LocationSettings locationSettings; @@ -625,6 +763,21 @@ class LocationDataPoint { pedStatus = ped; } + LocationDataPoint clone() { + return LocationDataPoint( + Position( + longitude: longitude, + latitude: latitude, + timestamp: timestamp, + accuracy: 0, + altitude: altitude, + heading: heading, + speed: speed, + speedAccuracy: 0), + steps, + pedStatus); + } + @override String toString() { return "LocationDataPoint at $timestamp { lat: $latitude lon: $longitude alt: $altitude dir: $heading° spe: $speed m/s with status: $pedStatus - $steps steps }"; @@ -740,6 +893,7 @@ class MinMax { T max; MinMax(this.min, this.max); + static MinMax fromList(List list) { double min = list.first; double max = list.first; @@ -765,7 +919,6 @@ extension Double on MinMax { } } - class LonLat { double longitude; double latitude; @@ -780,9 +933,48 @@ class LonLat { extension Range on MinMax { double get latRange { - return max.latitude-min.latitude; + return max.latitude - min.latitude; } + double get lngRange { - return max.longitude-min.longitude; + return max.longitude - min.longitude; + } +} + +extension DayIn on DateTime { + /// minutes from start of day + int dayInMinutes() { + return (hour * 60) + minute; + } + + /// milliseconds from start of day + int dayInMillis() { + return (hour * 3600 * 1000) + + (minute * 60 * 1000) + + (second * 1000) + + millisecond; + } +} + +extension NullableList on List { + /// find index of closest list entry that is not null + int? closestNonNull(int fromI) { + var walkedDistance = 1; + while (true) { + // negative/down walk index + var mI = fromI - walkedDistance; + // positive/up walk index + var pI = fromI + walkedDistance; + if (mI >= 0 && this[mI] != null) { + return mI; + } else if (pI < length && this[pI] != null) { + return pI; + } else { + walkedDistance++; + } + if (mI < 0 && pI >= length) { + return null; + } + } } } diff --git a/lib/src/presentation/components/helper.dart b/lib/src/presentation/components/helper.dart new file mode 100644 index 0000000..8b73849 --- /dev/null +++ b/lib/src/presentation/components/helper.dart @@ -0,0 +1,32 @@ + + +import 'package:flutter/material.dart'; + +class MapMarkerTriangle extends CustomPainter { + MapMarkerTriangle(); + + @override + void paint(Canvas canvas, Size size) { + var paint = Paint() + ..color = Colors.black + ..strokeWidth = 1; + + // Offset start = Offset(0, size.height / 2); + // Offset end = Offset(size.width, size.height / 2); + // + // canvas.drawLine(start, end, paint); + + var path = Path(); + path.moveTo(0, 11); + path.lineTo(18, 0); + path.lineTo(18, 38); + path.lineTo(0, 11); + path.close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } +} \ No newline at end of file diff --git a/lib/src/presentation/components/stats.dart b/lib/src/presentation/components/stats.dart index d7d565b..a0643a5 100644 --- a/lib/src/presentation/components/stats.dart +++ b/lib/src/presentation/components/stats.dart @@ -6,23 +6,21 @@ import 'package:geo_steps/src/presentation/components/lines.dart'; import 'package:geo_steps/src/utils/sizing.dart'; class OverviewTotals extends StatelessWidget { - final String timeFrameString; final int totalSteps; /// total distance in meters final double totalDistance; OverviewTotals( - {super.key, - required this.timeFrameString, - required this.totalSteps, - required this.totalDistance}); + {super.key, required this.totalSteps, required this.totalDistance}); @override Widget build(BuildContext context) { - var textStyle = const TextStyle(fontWeight: FontWeight.w500, fontSize: 24); + var textStyle = const TextStyle( + fontWeight: FontWeight.w500, fontSize: 24, color: Colors.white); return Container( decoration: BoxDecoration( + color: Colors.black, border: Border.all(color: Colors.black, width: 2), ), child: Padding( @@ -31,16 +29,6 @@ class OverviewTotals extends StatelessWidget { width: 140, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Totals - $timeFrameString", - style: - const TextStyle(fontWeight: FontWeight.w900, fontSize: 18), - ), - const Padding( - padding: EdgeInsets.only(top: 5, bottom: 10), - child: Line( - height: 2, - )), Text( "$totalSteps steps", style: textStyle, @@ -55,7 +43,7 @@ class OverviewTotals extends StatelessWidget { } } -class NamedBarGraph extends StatelessWidget { +class NamedBarChart extends StatelessWidget { final String title; final double height; final bool scrollable; @@ -66,7 +54,7 @@ class NamedBarGraph extends StatelessWidget { late List dataValues; late double max; - NamedBarGraph( + NamedBarChart( {super.key, required this.data, this.title = "geo_steps stats", @@ -77,11 +65,10 @@ class NamedBarGraph extends StatelessWidget { max = MinMax.fromList(dataValues.map((e) => e.toDouble()).toList()).max; } - @override Widget build(BuildContext context) { var textStyleOnWhite = const TextStyle( - color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500); + color: Colors.white, fontSize: 10, fontWeight: FontWeight.w700); var textStyleOnBlack = const TextStyle(fontSize: 14, fontWeight: FontWeight.w600); @@ -103,7 +90,10 @@ class NamedBarGraph extends StatelessWidget { (index) => Container( alignment: Alignment.center, height: rowHeight, - child: Text(dataKeys[index], style: textStyleOnBlack,)), + child: Text( + dataKeys[index], + style: textStyleOnBlack, + )), ), ), ), @@ -133,24 +123,16 @@ class NamedBarGraph extends StatelessWidget { (index) => Container( alignment: Alignment.center, height: rowHeight, - child: Text(dataValues[index].toString(), style: textStyleOnBlack)), + child: Text(dataValues[index].toString(), + style: textStyleOnBlack)), ), ), ), ], ), - Container( - height: 18, - color: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: textStyleOnWhite), - Text("${data.length} bars", style: textStyleOnWhite) - ], - ), - ) + ChartLegend( + left: Text(title, style: textStyleOnWhite), + right: Text("${data.length} bars", style: textStyleOnWhite)), ], ), ), @@ -158,7 +140,46 @@ class NamedBarGraph extends StatelessWidget { } } -class OverviewBarGraph extends StatelessWidget { +class ChartLegend extends StatelessWidget { + Widget left; + Widget right; + + ChartLegend({super.key, required this.left, required this.right}); + + @override + Widget build(BuildContext context) { + return Container( + height: 16, + color: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + left, + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 6), + child: Container( + decoration: const BoxDecoration( + border: Border.symmetric( + vertical: BorderSide(color: Colors.white, width: 3), + ), + image: DecorationImage( + fit: BoxFit.none, + scale: 4, + image: AssetImage("assets/line_pattern.jpg"), + repeat: ImageRepeat.repeat)), + ), + ), + ), + right, + ], + ), + ); + } +} + +class OverviewBarChart extends StatelessWidget { final String title; final double height; final bool scrollable; @@ -168,7 +189,7 @@ class OverviewBarGraph extends StatelessWidget { final List data; late MinMax valuesMinMax; - OverviewBarGraph( + OverviewBarChart( {super.key, required this.data, this.title = "geo_steps stats", @@ -181,7 +202,7 @@ class OverviewBarGraph extends StatelessWidget { @override Widget build(BuildContext context) { var textStyleOnWhite = const TextStyle( - color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500); + color: Colors.white, fontSize: 10, fontWeight: FontWeight.w700); var textStyleOnBlack = const TextStyle(fontSize: 14, fontWeight: FontWeight.w500); var sizer = SizeHelper(); @@ -204,11 +225,9 @@ class OverviewBarGraph extends StatelessWidget { children: List.generate(4, (i) { String value; if (i == 0) { - value = - valuesMinMax.min.toStringAsFixed(1); + value = valuesMinMax.min.toStringAsFixed(1); } else if (i == 3) { - value = - valuesMinMax.max.toStringAsFixed(1); + value = valuesMinMax.max.toStringAsFixed(1); } else { value = (valuesMinMax.diff / 3 * i) .toStringAsFixed(1); @@ -234,18 +253,9 @@ class OverviewBarGraph extends StatelessWidget { ], ), ))), - Container( - height: 18, - color: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: textStyleOnWhite), - Text("${data.length} bars", style: textStyleOnWhite) - ], - ), - ) + ChartLegend( + left: Text(title, style: textStyleOnWhite), + right: Text("${data.length} bars", style: textStyleOnWhite)), ], ), ), @@ -293,24 +303,41 @@ class BarChart extends StatelessWidget { } class HourlyActivity extends StatefulWidget { - final double hourWidth = 50; + final double hourWidth = 60; + final double hourPad = 5; + final List data; + late double max; + final int initialHour; + + Function(double)? onScroll = (percentage) {}; - const HourlyActivity({super.key}); + HourlyActivity( + {super.key, required this.data, this.onScroll, this.initialHour = 12}) { + max = MinMax.fromList(data).max; + } @override State createState() => _HourlyActivityState(); } class _HourlyActivityState extends State { - ScrollController scrollController = - ScrollController(initialScrollOffset: 715); + ScrollController scrollController = ScrollController(initialScrollOffset: 0); bool isScrolling = false; int selectedHourIndex = 0; @override void initState() { + setState(() { + selectedHourIndex = widget.initialHour; + }); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + scrollController.animateTo( + (widget.hourWidth + widget.hourPad) * selectedHourIndex + + widget.hourWidth / 2, + duration: const Duration(seconds: 1), + curve: Curves.decelerate); scrollController.addListener(() { + onScroll(); if (scrollController.position.pixels == scrollController.position.maxScrollExtent || scrollController.position.pixels == @@ -321,6 +348,7 @@ class _HourlyActivityState extends State { scrollController.position.isScrollingNotifier.addListener(() { if (scrollController.positions.isNotEmpty) { var scrollBool = scrollController.position.isScrollingNotifier.value; + if (scrollBool != isScrolling) { setState(() { isScrolling = scrollBool; @@ -331,15 +359,25 @@ class _HourlyActivityState extends State { } } }); - setSelectedHour(); }); super.initState(); } + onScroll() { + final pixels = scrollController.position.pixels; + final percentage = pixels / + (scrollController.position.maxScrollExtent + + 0.1); // +0.1 so its never 24 + widget.onScroll!(percentage); + } + setSelectedHour() { - var pixels = scrollController.position.pixels + widget.hourWidth / 2; - int newIndex = - (pixels / scrollController.position.maxScrollExtent * 23).floor(); + final pixels = scrollController.position.pixels; + int newIndex = (pixels / + (scrollController.position.maxScrollExtent + widget.hourPad / 2) * + 24) + .floor(); + setState(() { selectedHourIndex = newIndex; }); @@ -352,40 +390,18 @@ class _HourlyActivityState extends State { width: sizer.width, height: 139, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 0), + padding: + EdgeInsets.symmetric(vertical: widget.hourPad, horizontal: 0), child: ListView.builder( itemCount: 24, padding: EdgeInsets.symmetric( - horizontal: sizer.width / 2 - 25, vertical: 0), + horizontal: sizer.width / 2, vertical: 0), scrollDirection: Axis.horizontal, controller: scrollController, itemBuilder: (BuildContext context, int index) { - List hoursPercent = [ - 0, - 0, - 0, - 0, - 0, - .1, - .4, - .6, - 0, - 0, - .1, - 0, - .3, - 1, - .5, - 1, - .1, - .3, - .2, - 0, - 0, - 0, - 0, - 0 - ]; // TODO: use real data + List hoursPercent = widget.data + .map((h) => widget.max == 0 ? 0.0 : h / widget.max) + .toList(); return Container( padding: index < 23 ? const EdgeInsets.only(right: 5) @@ -403,32 +419,59 @@ class _HourlyActivityState extends State { height: 3), SizedBox( height: 105, - child: Column(children: [ - Expanded( - child: Container( - width: widget.hourWidth, - decoration: BoxDecoration( - border: Border.all( - color: Colors.black, width: 1)), - )), - Container( - width: widget.hourWidth, - height: 105 * hoursPercent[index], - color: index == selectedHourIndex - ? null - : Colors.black, - decoration: index == selectedHourIndex - ? BoxDecoration( - border: Border.all( - color: Colors.black, width: 1), - image: const DecorationImage( - fit: BoxFit.none, - scale: 2.5, - image: AssetImage( - "assets/line_pattern.jpg"), - repeat: ImageRepeat.repeat)) - : null), - ])), + child: Stack( + children: [ + Column(children: [ + Expanded( + child: Container( + width: widget.hourWidth, + decoration: BoxDecoration( + border: Border.all( + color: Colors.black, width: 1)), + )), + Container( + width: widget.hourWidth, + height: 105 * hoursPercent[index], + constraints: const BoxConstraints( + minHeight: 0, maxHeight: 105), + color: index == selectedHourIndex + ? null + : Colors.black, + decoration: index == selectedHourIndex + ? BoxDecoration( + border: Border.all( + color: Colors.black, width: 1), + image: const DecorationImage( + fit: BoxFit.none, + scale: 2.5, + image: AssetImage( + "assets/line_pattern.jpg"), + repeat: ImageRepeat.repeat)) + : null), + ]), + Positioned( + top: 80 * (1 - hoursPercent[index]), + child: SizedBox( + width: widget.hourWidth, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + height: 16, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(), + ), + child: Center( + child: Text( + widget.data[index] + .toStringAsFixed(0), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500), + ))), + ))), + ], + )), SizedBox(height: 16, child: Text("$index")) ], ), diff --git a/lib/src/presentation/home.dart b/lib/src/presentation/home.dart index c225985..c8e201e 100644 --- a/lib/src/presentation/home.dart +++ b/lib/src/presentation/home.dart @@ -32,10 +32,9 @@ class _MyHomePageState extends State { locationService = LocationService(); AppSettings.instance.trackingLocation.get().then((value) { - setState(() { - isTrackingLocation = value; - locationService.init().then((value) => locationService.loadToday()); - }); + isTrackingLocation = value; + locationService.init().then((value) => + locationService.loadToday().then((value) => setState(() {}))); }); } @@ -51,13 +50,14 @@ class _MyHomePageState extends State { Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, - children: const [ - Text("3.742", - style: - TextStyle(fontSize: 75, fontWeight: FontWeight.w900)), - Text("10,2 km", - style: - TextStyle(fontSize: 40, fontWeight: FontWeight.w700)), + children: [ + Text(locationService.stepsTotal.toString(), + style: const TextStyle( + fontSize: 75, fontWeight: FontWeight.w900)), + Text( + "${(locationService.distanceTotal / 1000).toStringAsFixed(1)} km", + style: const TextStyle( + fontSize: 40, fontWeight: FontWeight.w700)), ], ), Container( @@ -95,34 +95,42 @@ class _MyHomePageState extends State { style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700)), const Padding(padding: EdgeInsets.only(top: 12)), - Row( - children: const [ - Text("24 ", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w700)), - Text("days", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w400)), - ], - ), - const Padding(padding: EdgeInsets.only(top: 6)), - Row( - children: const [ - Text("154.00 ", + (isTrackingLocation != true) + ? const Text("tracking currently stopped", textAlign: TextAlign.left, style: TextStyle( fontSize: 24, - fontWeight: FontWeight.w700)), - Text("steps", - textAlign: TextAlign.left, - style: - TextStyle(fontWeight: FontWeight.w400)), - ], - ), + fontWeight: FontWeight.w400)) + : Row( + children: const [ + Text("24 ", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700)), + Text("days", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w400)), + ], + ), + if (isTrackingLocation == true) + const Padding(padding: EdgeInsets.only(top: 6)), + if (isTrackingLocation == true) + Row( + children: const [ + Text("154.00 ", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w700)), + Text("steps", + textAlign: TextAlign.left, + style: + TextStyle(fontWeight: FontWeight.w400)), + ], + ), SizedBox( height: 40, child: Row( @@ -187,7 +195,8 @@ class _MyHomePageState extends State { ), ], ))), - if (locationService.isInitialized) ActivityMap(locationService: locationService), + if (locationService.isInitialized) + ActivityMap(locationService: locationService), const Padding(padding: EdgeInsets.only(bottom: 50)), ]); } @@ -209,11 +218,13 @@ class _ActivityMapState extends State { void initState() { super.initState(); widget.locationService.loadToday().then((wasLoaded) { - if (wasLoaded) { - setState(() { - dataToday = widget.locationService.dataPoints; - }); - } + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (wasLoaded) { + setState(() { + dataToday = widget.locationService.dataPoints; + }); + } + }); }); } diff --git a/lib/src/presentation/homepoints.dart b/lib/src/presentation/homepoints.dart index e3f5f09..00723d3 100644 --- a/lib/src/presentation/homepoints.dart +++ b/lib/src/presentation/homepoints.dart @@ -169,7 +169,7 @@ class AddHomepointModal extends StatefulWidget { } class _AddHomepointModalState extends State { - late MapController mapController; + late MapController mapController = MapController(); String name = "homepoint 1"; double radius = 80; LatLng? point; @@ -188,13 +188,15 @@ class _AddHomepointModalState extends State { }); } - mapController = MapController(); - Geolocator.getLastKnownPosition().then((pos) { - mapController.move(LatLng(pos!.latitude, pos!.longitude), 14); - }); - Geolocator.getCurrentPosition().then((pos) { - mapController.move(LatLng(pos.latitude, pos.longitude), 14); - }); + if (widget.basedOn != null) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + mapController.move(widget.basedOn!.position, 14.75); + }); + } else { + Geolocator.getLastKnownPosition().then((pos) { + mapController.move(LatLng(pos!.latitude, pos!.longitude), 14); + }); + } } void submit() { diff --git a/lib/src/presentation/overview.dart b/lib/src/presentation/overview.dart index 25e909e..1380a9c 100644 --- a/lib/src/presentation/overview.dart +++ b/lib/src/presentation/overview.dart @@ -204,9 +204,8 @@ class _OverviewPageState extends State { child: Row( children: [ OverviewTotals( - timeFrameString: timeFrameString, - totalSteps: 6929, - totalDistance: 4200, + totalSteps: locationService!.stepsTotal, + totalDistance: locationService!.distanceTotal, ), Expanded(child: Container()), ], @@ -215,7 +214,7 @@ class _OverviewPageState extends State { ActivityMap(data: locationService!.dataPoints), Padding( padding: const EdgeInsets.all(10), - child: OverviewBarGraph( + child: OverviewBarChart( scrollable: true, data: [1, 2, 6, 2, 3, 1, 12, 42, 10, 1, 1, 3, 95, 32])), ], diff --git a/lib/src/presentation/today.dart b/lib/src/presentation/today.dart index 098e355..fb50814 100644 --- a/lib/src/presentation/today.dart +++ b/lib/src/presentation/today.dart @@ -1,10 +1,13 @@ import 'dart:developer'; import 'dart:async'; +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:latlong2/latlong.dart'; +import 'package:geo_steps/src/application/homepoints.dart'; +import 'package:geo_steps/src/presentation/components/helper.dart'; +import 'package:latlong2/latlong.dart' as latlng; import 'package:intl/intl.dart'; // local imports @@ -51,12 +54,21 @@ class _TodaysMapState extends State with SingleTickerProviderStateMixin { late Animation detailsAnimation; late AnimationController detailsController; + bool showDetails = false; late LocationService locationService; late TargetPlatform defaultTargetPlatform = TargetPlatform.iOS; + HomepointManager? homepointManager; + final mapController = MapController(); - bool showDetails = false; + double zoomLevel = 12.8; static const double mapHeightDetails = 150; + latlng.LatLng markerPosition = latlng.LatLng(0, 0); + List homepointCircles = []; + + LocationDataPoint? selectedMinute; + List? minutes; + int? initialHourIndex; @override void initState() { @@ -68,21 +80,29 @@ class _TodaysMapState extends State .whenComplete(() => locationService.loadToday().then((wasLoaded) { if (wasLoaded && locationService.hasPositions) { setState(() => mapController.move( - LatLng(locationService.lastPos!.latitude, + latlng.LatLng(locationService.lastPos!.latitude, locationService.lastPos!.longitude), - 12.8)); + zoomLevel)); + minutes = locationService.dataPointPerMinute; + + final firstP = locationService.dataPoints.first; + markerPosition = + latlng.LatLng(firstP.latitude, firstP.longitude); + + initialHourIndex = locationService.hourlyStepsTotal + .indexOf(locationService.hourlyStepsTotal.reduce(math.max)); } })); AppSettings().trackingLocation.get().then((isTrackingLocation) { - if (isTrackingLocation != null && isTrackingLocation) { + if (isTrackingLocation != true) { FlutterBackgroundService().on("sendTrackingData").listen((event) { setState(() { List receivedDataPoints = event!["trackingData"]; var changed = locationService.dataPointsFromKV(receivedDataPoints); if (changed) { mapController.move( - LatLng(locationService.lastPos!.latitude, + latlng.LatLng(locationService.lastPos!.latitude, locationService.lastPos!.longitude), 14.5); } @@ -108,6 +128,32 @@ class _TodaysMapState extends State ..addListener(() { setState(() {}); }); + + homepointManager = HomepointManager(); + homepointManager! + .init() + .then((value) => homepointManager!.load().then((value) => setState(() { + homepointManager!.getVisits(locationService.dataPoints); + + homepointManager!.homepoints.forEach((key, point) { + homepointCircles.addAll([ + CircleMarker( + point: point.position, + radius: point.radius, + useRadiusInMeter: true, + color: Colors.white.withOpacity(0.3), + borderColor: Colors.black, + borderStrokeWidth: 2, + ), + CircleMarker( + point: point.position, + radius: point.radius / 3, + useRadiusInMeter: true, + color: Colors.white.withOpacity(0.4), + ) + ]); + }); + }))); } @override @@ -146,7 +192,15 @@ class _TodaysMapState extends State onMapReady: () { // set initial position on map }, - zoom: 13.0, + onMapEvent: (e) { + // set zoomLevel on change + if (e.runtimeType == MapEventMoveEnd) { + setState(() { + zoomLevel = e.zoom; + }); + } + }, + zoom: zoomLevel, maxZoom: 19.0, keepAlive: true, interactiveFlags: // all interactions except rotation @@ -169,6 +223,9 @@ class _TodaysMapState extends State // TileLayer(urlTemplate: "https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/4/5/5?blankTile=false", // userAgentPackageName: 'dev.janeuster.geo_steps'), // ), + CircleLayer( + circles: homepointCircles, + ), PolylineLayer( polylineCulling: false, polylines: [ @@ -190,8 +247,7 @@ class _TodaysMapState extends State Marker( width: 46, height: 46, - point: LatLng(locationService.lastPos!.latitude, - locationService.lastPos!.longitude), + point: markerPosition, builder: (context) => Transform.translate( offset: const Offset(0, -23), child: Container( @@ -199,7 +255,16 @@ class _TodaysMapState extends State image: DecorationImage( image: AssetImage( "assets/map_pin.png")))), - )) + )), + if (selectedMinute != null) + LocationDetailsMarker( + markerPosition, selectedMinute!), + // Container( + // width: 40, + // height: 40, + // decoration: + // BoxDecoration(color: Colors.white, border: Border.all()), + // ) ], ) ], @@ -228,7 +293,7 @@ class _TodaysMapState extends State const Padding( padding: EdgeInsets.only(bottom: 5)), Transform.rotate( - angle: showDetails ? 0 : 1 * pi, + angle: showDetails ? 0.0 : 1.0 * math.pi, child: const Icon( Icomoon.arrow, color: Colors.white, @@ -249,9 +314,9 @@ class _TodaysMapState extends State children: showDetails ? [ OverviewTotals( - timeFrameString: "Today", - totalSteps: 6929, - totalDistance: 4200, + totalSteps: locationService.stepsTotal, + totalDistance: + locationService.distanceTotal, ), Expanded(child: Container()), ] @@ -268,7 +333,38 @@ class _TodaysMapState extends State height: 2, ) : null), - const HourlyActivity(), + if (initialHourIndex != null) + HourlyActivity( + data: locationService.hourlyStepsTotal, + initialHour: initialHourIndex!, + onScroll: (percentage) { + var thisDate = DateTime.now(); + final millisecondsToday = + (percentage * 24 * 60 * 60 * 1000).round(); + final minutesToday = + (percentage * ((24 * 60) - 1)).round(); + thisDate = DateTime( + thisDate.year, thisDate.month, thisDate.day); + thisDate = DateTime.fromMillisecondsSinceEpoch( + thisDate.millisecondsSinceEpoch + + millisecondsToday); + + if (locationService.hasPositions) { + // log("$minutes"); + // var newPos = locationService.dataPointClosestTo(thisDate.toLocal()); + // log("$newPos"); + if (minutes != null) { + setState(() { + selectedMinute = minutes![minutesToday]; + markerPosition = latlng.LatLng( + selectedMinute!.latitude, + selectedMinute!.longitude); + mapController.move(markerPosition, zoomLevel); + }); + } + } + }, + ), if (showDetails) ...[ const Padding( padding: EdgeInsets.symmetric( @@ -277,31 +373,22 @@ class _TodaysMapState extends State height: 2, )), Padding( - padding: const EdgeInsets.all(10), - child: OverviewBarGraph(data: const [ - 1, - 2, - 6, - 2, - 3, - 1, - 12, - 42, - 10, - 1, - 1, - 3, - 95, - 32 - ], title: "stat 1")), - Padding( - padding: const EdgeInsets.all(8.0), - child: NamedBarGraph( - data: {"home": 4, "a": 1, "b": 2} - .entries - .toList(), - title: "stat 2"), - ), + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 12), + child: OverviewBarChart( + data: locationService.hourlyDistanceTotal + .map((e) => e / 1000) + .toList(), + title: "hourly average speed in km/h")), + if (homepointManager != null && + homepointManager!.visits != null) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 12), + child: NamedBarChart( + data: homepointManager!.visits!, + title: "visited homepoints today"), + ), ], ], ), @@ -315,3 +402,54 @@ class _TodaysMapState extends State ); } } + +Marker LocationDetailsMarker( + latlng.LatLng markerPosition, LocationDataPoint selectedMinute) { + return Marker( + point: markerPosition, + builder: (context) => Transform.translate( + offset: const Offset(28, -30), + child: SizedBox( + height: 40, + child: Stack( + clipBehavior: Clip.none, + children: [ + SizedBox(child: CustomPaint(painter: MapMarkerTriangle())), + Positioned( + left: 18, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 2), + width: 60, + height: 38, + decoration: const BoxDecoration( + color: Colors.white, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (selectedMinute.timestamp != null) + Text( + DateFormat("HH:mm") + .format(selectedMinute.timestamp!), + style: const TextStyle( + fontSize: 9, fontWeight: FontWeight.w500), + ), + Text( + selectedMinute.pedStatus, + style: const TextStyle( + fontSize: 9, fontWeight: FontWeight.w500), + ), + Text( + "${selectedMinute.steps ?? 0} steps", + style: const TextStyle( + fontSize: 9, fontWeight: FontWeight.w500), + ) + ], + ), + )) + ], + ), + ), + )); +}