Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch tide data from public API #424 #852

Merged
merged 9 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add unit tests
  • Loading branch information
cohenadair committed Aug 9, 2023
commit 366f14b82a482aae3f40056271b09e5714bf52da
2 changes: 1 addition & 1 deletion mobile/lib/atmosphere_fetcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 15,7 @@ import 'utils/network_utils.dart';
import 'utils/number_utils.dart';
import 'utils/protobuf_utils.dart';
import 'utils/string_utils.dart';
import 'widgets/fetcher_input.dart';
import 'widgets/fetch_input_header.dart';
import 'wrappers/http_wrapper.dart';

class AtmosphereFetcher {
Expand Down
5 changes: 4 additions & 1 deletion mobile/lib/pages/save_catch_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 216,14 @@ class SaveCatchPageState extends State<SaveCatchPage> {
_fields[field.id] = field;
}

// Need to set this here (rather than exclusively in EditableFormPage) so
// Need to set these here (rather than exclusively in EditableFormPage) so
// the auto-fetch atmosphere method is invoked correctly.
_fields[catchFieldIdAtmosphere]!.isShowing = _userPreferenceManager
.catchFieldIds.isEmpty ||
_userPreferenceManager.catchFieldIds.contains(catchFieldIdAtmosphere);
_fields[catchFieldIdTide]!.isShowing =
_userPreferenceManager.catchFieldIds.isEmpty ||
_userPreferenceManager.catchFieldIds.contains(catchFieldIdTide);

_waterDepthInputState = MultiMeasurementInputSpec.waterDepth(context);
_waterTemperatureInputState =
Expand Down
27 changes: 19 additions & 8 deletions mobile/lib/tide_fetcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 4,7 @@ import 'package:mapbox_gl/mapbox_gl.dart';
import 'package:mobile/i18n/strings.dart';
import 'package:mobile/utils/date_time_utils.dart';
import 'package:mobile/utils/map_utils.dart';
import 'package:mobile/utils/protobuf_utils.dart';
import 'package:quiver/strings.dart';
import 'package:timezone/timezone.dart';

Expand All @@ -13,15 14,14 @@ import 'model/gen/anglerslog.pb.dart';
import 'properties_manager.dart';
import 'utils/network_utils.dart';
import 'utils/number_utils.dart';
import 'widgets/fetcher_input.dart';
import 'widgets/fetch_input_header.dart';
import 'wrappers/http_wrapper.dart';

class TideFetcher {
static const _authority = "worldtides.info";
static const _path = "/api/v3";

final _log = const Log("TideFetcher");

final Log log;
final AppManager appManager;
final TZDateTime dateTime;
final LatLng? latLng;
Expand All @@ -30,7 30,12 @@ class TideFetcher {

PropertiesManager get _propertiesManager => appManager.propertiesManager;

TideFetcher(this.appManager, this.dateTime, this.latLng);
TideFetcher(
this.appManager,
this.dateTime,
this.latLng, {
this.log = const Log("TideFetcher"),
});

Future<FetchResult<Tide?>> fetch([Strings? strings]) async {
if (latLng == null) {
Expand All @@ -50,26 55,31 @@ class TideFetcher {
errorMessage: strings?.tideFetcherErrorNoLocationFound,
);
} else if (isNotEmpty(error)) {
_log.e(StackTrace.current, "Tide fetch error: $error");
log.e(StackTrace.current, "Tide fetch error: $error");
return FetchResult();
}

var heights = json["heights"];
if (heights is! List) {
_log.e(StackTrace.current, "Tide fetch is missing heights");
log.e(StackTrace.current, "Tide fetch is missing heights");
return FetchResult();
}

var extremes = json["extremes"];
if (extremes is! List) {
_log.e(StackTrace.current, "Tide fetch is missing extremes");
log.e(StackTrace.current, "Tide fetch is missing extremes");
return FetchResult();
}

var tide = Tide(timeZone: dateTime.locationName);
_parseJsonHeights(tide, heights);
_parseJsonExtremes(tide, extremes);

if (!tide.isValid) {
log.e(StackTrace.current, "Fetched invalid tide value");
return FetchResult();
}

return FetchResult(data: tide);
}

Expand Down Expand Up @@ -175,7 185,6 @@ class TideFetcher {
var params = {
"heights": null,
"extremes": null,
"plots": null,
"date": DateFormat("yyyy-MM-dd").format(dateTime),
"lat": latLng!.latitudeString,
"lon": latLng!.longitudeString,
Expand All @@ -185,6 194,8 @@ class TideFetcher {
return await getRestJson(
_httpWrapper,
Uri.https(_authority, _path, params),
// Error responses result in HTTP error codes, but we still need to read
// result.
returnNullOnHttpError: false,
);
}
Expand Down
22 changes: 16 additions & 6 deletions mobile/lib/utils/protobuf_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -545,8 545,13 @@ extension MultiMeasurements on MultiMeasurement {
}) {
String formatResult(String result) {
if (isNotEmpty(resultFormat)) {
return format(resultFormat!, [result]);
result = format(resultFormat!, [result]);
}

if (isNegative) {
result = "-$result";
}

return result;
}

Expand Down Expand Up @@ -579,10 584,6 @@ extension MultiMeasurements on MultiMeasurement {
return formatResult(ifZero!);
}

if (isNegative) {
result = "-$result";
}

return formatResult(result);
}

Expand Down Expand Up @@ -1126,7 1127,7 @@ extension Units on Unit {
if (value < 0) {
avgWhole = value.ceilToDouble();

// Handles the case where the overall value is between 0 and -1.
// Handles the case where the overall value is between -1 and 0.
result.isNegative = true;
}

Expand Down Expand Up @@ -1856,6 1857,15 @@ extension Tides on Tide {
// Industry standard.
static int get displayDecimalPlaces => 3;

bool get isValid =>
hasType() ||
hasHeight() ||
hasFirstLowTimestamp() ||
hasFirstHighTimestamp() ||
hasSecondLowTimestamp() ||
hasSecondHighTimestamp() ||
daysHeights.isNotEmpty;

TZDateTime firstLowDateTime(BuildContext context) =>
TimeManager.of(context).dateTime(firstLowTimestamp.toInt(), timeZone);

Expand Down
4 changes: 2 additions & 2 deletions mobile/lib/widgets/atmosphere_input.dart
Original file line number Diff line number Diff line change
@@ -1,7 1,7 @@
import 'package:fixnum/fixnum.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mobile/widgets/fetcher_input.dart';
import 'package:mobile/widgets/fetch_input_header.dart';

import '../atmosphere_fetcher.dart';
import '../i18n/strings.dart';
Expand Down Expand Up @@ -268,7 268,7 @@ class __AtmosphereInputPageState extends State<_AtmosphereInputPage> {
}

Widget _buildHeader() {
return FetcherInput<Atmosphere>(
return FetchInputHeader<Atmosphere>(
fishingSpot: widget.fishingSpot,
defaultErrorMessage: Strings.of(context).atmosphereInputFetchError,
dateTime: widget.fetcher.dateTime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 25,15 @@ class FetchResult<T> {
});
}

class FetcherInput<T> extends StatefulWidget {
class FetchInputHeader<T> extends StatefulWidget {
final FishingSpot? fishingSpot;
final String defaultErrorMessage;
final TZDateTime dateTime;
final Future<FetchResult<T?>> Function() onFetch;
final void Function(T) onFetchSuccess;
final InputController<T> controller;

const FetcherInput({
const FetchInputHeader({
this.fishingSpot,
required this.defaultErrorMessage,
required this.dateTime,
Expand All @@ -43,10 43,10 @@ class FetcherInput<T> extends StatefulWidget {
});

@override
State<FetcherInput<T>> createState() => _FetcherInputState<T>();
State<FetchInputHeader<T>> createState() => _FetchInputHeaderState<T>();
}

class _FetcherInputState<T> extends State<FetcherInput<T>> {
class _FetchInputHeaderState<T> extends State<FetchInputHeader<T>> {
bool _isLoading = false;

FishingSpotManager get _fishingSpotManager => FishingSpotManager.of(context);
Expand Down
9 changes: 3 additions & 6 deletions mobile/lib/widgets/multi_measurement_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -273,18 273,15 @@ class MultiMeasurementInputSpec {
title ?? Strings.of(context).catchFieldWaterDepthLabel,
);

MultiMeasurementInputSpec.tideHeight(
BuildContext context, {
String? title,
}) : this._(
MultiMeasurementInputSpec.tideHeight(BuildContext context)
: this._(
context,
imperialUnit: (_) => Unit.feet,
metricUnit: Unit.meters,
fractionUnit: Unit.inches,
system: (context) =>
UserPreferenceManager.of(context).tideHeightSystem,
title: (context) =>
title ?? Strings.of(context).catchFieldTideHeightLabel,
title: (context) => Strings.of(context).catchFieldTideHeightLabel,
);

MultiMeasurementInputSpec.waterTemperature(BuildContext context)
Expand Down
14 changes: 5 additions & 9 deletions mobile/lib/widgets/tide_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 16,7 @@ import '../tide_fetcher.dart';
import '../utils/page_utils.dart';
import '../utils/protobuf_utils.dart';
import 'date_time_picker.dart';
import 'fetcher_input.dart';
import 'fetch_input_header.dart';
import 'input_controller.dart';
import 'list_item.dart';
import 'list_picker_input.dart';
Expand Down Expand Up @@ -52,7 52,7 @@ class TideInputState extends State<TideInput> {
}

var extremes = tide.extremesDisplayValue(context);
if (extremes.isNotEmpty) {
if (isNotEmpty(extremes)) {
subtitle2 = Text(extremes);
}
}
Expand Down Expand Up @@ -168,7 168,7 @@ class __TideInputPageState extends State<_TideInputPage> {
}

Widget _buildHeader() {
return FetcherInput<Tide>(
return FetchInputHeader<Tide>(
fishingSpot: widget.fishingSpot,
defaultErrorMessage: Strings.of(context).atmosphereInputFetchError,
dateTime: widget.dateTime,
Expand All @@ -183,7 183,7 @@ class __TideInputPageState extends State<_TideInputPage> {
duration: animDurationDefault,
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: _controller.hasValue
child: _hasValue
? Padding(
padding: insetsHorizontalDefaultTopSmall,
child: TideChart(_controller.value!, isSummary: false),
Expand Down Expand Up @@ -300,13 300,9 @@ class __TideInputPageState extends State<_TideInputPage> {
).fetch(Strings.of(context));
}

void _updateFromTide(Tide? tide) {
void _updateFromTide(Tide tide) {
_controller.value = tide;

if (tide == null) {
return;
}

if (tide.hasFirstLowTimestamp()) {
_firstLowTideController.value = tide.firstLowDateTime(context);
}
Expand Down
7 changes: 7 additions & 0 deletions mobile/test/mocks/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 20,7 @@ import 'package:mobile/bait_manager.dart';
import 'package:mobile/body_of_water_manager.dart';
import 'package:mobile/catch_manager.dart';
import 'package:mobile/gps_trail_manager.dart';
import 'package:mobile/log.dart';
import 'package:mobile/model/gen/anglerslog.pb.dart';
import 'package:mobile/poll_manager.dart';
import 'package:mobile/report_manager.dart';
Expand Down Expand Up @@ -108,6 109,12 @@ Trip_CatchesPerEntity newInputItemShim(dynamic pickerItem) =>
@GenerateMocks([IOSink])
@GenerateMocks([LocalDatabaseManager])
@GenerateMocks([LocationMonitor])
@GenerateMocks([], customMocks: [
MockSpec<Log>(unsupportedMembers: {
Symbol("sync"),
Symbol("async"),
})
])
@GenerateMocks([MethodManager])
@GenerateMocks([PlatformException])
@GenerateMocks([PollManager])
Expand Down
Loading