Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ YetAnotherBusApp(YABus)是一個以 Flutter 開發的跨平台公車動態
- 快速查看常用公車
- 個人化首頁

### 捷運最近車站

- 在捷運分頁點選「定位最近的車站」,跨系統尋找最近站點,自動選擇系統、路線與車站並開啟地圖及到站資訊。
- 定位僅在點選後使用,距離在裝置端計算;若部分系統資料暫時無法取得,仍顯示可用結果並提示查詢範圍不完整。
- 定位失敗時保留目前選擇,仍可手動切換系統與路線。

### 地圖顯示

- 顯示公車即時位置
Expand Down
128 changes: 128 additions & 0 deletions lib/core/nearest_metro_station.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import 'package:geolocator/geolocator.dart';

import 'transit_repository.dart';

/// The static data needed to select a station and its serving line together.
class MetroSystemData {
const MetroSystemData({
required this.system,
required this.lines,
required this.stations,
required this.stationOfLine,
});

final MetroSystem system;
final List<MetroLine> lines;
final List<MetroStation> stations;
final List<MetroStationOfLine> stationOfLine;
}

Future<MetroSystemData> loadMetroSystemData(
TransitRepository repository,
MetroSystem system,
) async {
final results = await Future.wait([
repository.getMetroLines(system.system),
repository.getMetroStations(system.system),
repository.getMetroStationOfLine(system.system),
]);
return MetroSystemData(
system: system,
lines: results[0] as List<MetroLine>,
stations: results[1] as List<MetroStation>,
stationOfLine: results[2] as List<MetroStationOfLine>,
);
}

class NearestMetroStation {
const NearestMetroStation({
required this.data,
required this.station,
required this.line,
required this.distanceMeters,
});

final MetroSystemData data;
final MetroStation station;
final MetroLine line;
final double distanceMeters;
}

/// Finds the nearest station that can be displayed on an available metro line.
///
/// Station-of-line membership is authoritative: the stations endpoint can
/// return an empty `line_id`. At equal distances, keep the preferred system
/// and then its preferred line (useful at interchanges); otherwise retain input
/// order. A preference never overrides a closer station.
NearestMetroStation? nearestMetroStation(
List<MetroSystemData> systems, {
required double latitude,
required double longitude,
String? preferredSystem,
String? preferredLineId,
}) {
if (!_validCoordinates(latitude, longitude)) {
return null;
}

NearestMetroStation? nearest;
var bestPreference = -1;
for (final data in systems) {
final linesById = {
for (final line in data.lines)
if (line.lineId.isNotEmpty) line.lineId: line,
};
final stationLines = <String, Set<String>>{};
for (final entry in data.stationOfLine) {
if (!linesById.containsKey(entry.lineId)) {
continue;
}
for (final station in entry.stations) {
stationLines
.putIfAbsent(station.stationId, () => <String>{})
.add(entry.lineId);
}
}

for (final station in data.stations) {
if (station.stationId.isEmpty ||
!_validCoordinates(station.lat, station.lon) ||
(station.lat == 0 && station.lon == 0)) {
continue;
}
final lineIds = stationLines[station.stationId];
if (lineIds == null) {
continue;
}
final distance = Geolocator.distanceBetween(
latitude,
longitude,
station.lat,
station.lon,
);
for (final lineId in lineIds) {
final preferred = data.system.system == preferredSystem;
final preference = preferred ? (lineId == preferredLineId ? 2 : 1) : 0;
if (nearest == null ||
distance < nearest.distanceMeters ||
(distance == nearest.distanceMeters &&
preference > bestPreference)) {
nearest = NearestMetroStation(
data: data,
station: station,
line: linesById[lineId]!,
distanceMeters: distance,
);
bestPreference = preference;
}
}
}
}
return nearest;
}

bool _validCoordinates(double latitude, double longitude) =>
latitude.isFinite &&
longitude.isFinite &&
latitude.abs() <= 90 &&
longitude.abs() <= 180;
12 changes: 10 additions & 2 deletions lib/core/user_location.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class LocationFailure implements Exception {
const LocationFailure(
this.message, {
this.deniedForever = false,
this.permissionDenied = false,
this.serviceDisabled = false,
});

Expand All @@ -15,6 +16,9 @@ class LocationFailure implements Exception {
/// The user has to go into system settings; asking again will not help.
final bool deniedForever;

/// Location access was denied, including when requesting again is possible.
final bool permissionDenied;

/// The device-wide location switch is off, so app permission settings alone
/// cannot resolve the failure.
final bool serviceDisabled;
Expand Down Expand Up @@ -43,10 +47,14 @@ Future<Position> resolveUserPosition({
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.deniedForever) {
throw const LocationFailure('沒有取得定位權限。', deniedForever: true);
throw const LocationFailure(
'沒有取得定位權限。',
deniedForever: true,
permissionDenied: true,
);
}
if (permission == LocationPermission.denied) {
throw const LocationFailure('沒有取得定位權限。');
throw const LocationFailure('沒有取得定位權限。', permissionDenied: true);
}

Position? lastKnown;
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,10 @@
"@metroFrequencyOnlyEstimate": {"placeholders": {"min": {"type": "int"}, "max": {"type": "int"}}},
"metroDestination": "Toward {destination}",
"@metroDestination": {"placeholders": {"destination": {"type": "String"}}},
"metroLocateNearest": "Find nearest station",
"metroLocating": "Finding nearest station…",
"metroNearbyPartial": "Some metro systems could not be checked. Showing the nearest station from the available data.",
"metroStationLookupFailed": "Could not load metro stations. Please try again.",
"railOperatingNotices": "Service notices",
"railAllStations": "All stations",
"railOtherStations": "Other",
Expand Down
24 changes: 24 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5368,6 +5368,30 @@ abstract class AppLocalizations {
/// **'Toward {destination}'**
String metroDestination(String destination);

/// No description provided for @metroLocateNearest.
///
/// In en, this message translates to:
/// **'Find nearest station'**
String get metroLocateNearest;

/// No description provided for @metroLocating.
///
/// In en, this message translates to:
/// **'Finding nearest station…'**
String get metroLocating;

/// No description provided for @metroNearbyPartial.
///
/// In en, this message translates to:
/// **'Some metro systems could not be checked. Showing the nearest station from the available data.'**
String get metroNearbyPartial;

/// No description provided for @metroStationLookupFailed.
///
/// In en, this message translates to:
/// **'Could not load metro stations. Please try again.'**
String get metroStationLookupFailed;

/// No description provided for @railOperatingNotices.
///
/// In en, this message translates to:
Expand Down
14 changes: 14 additions & 0 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3308,6 +3308,20 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Toward $destination';
}

@override
String get metroLocateNearest => 'Find nearest station';

@override
String get metroLocating => 'Finding nearest station…';

@override
String get metroNearbyPartial =>
'Some metro systems could not be checked. Showing the nearest station from the available data.';

@override
String get metroStationLookupFailed =>
'Could not load metro stations. Please try again.';

@override
String get railOperatingNotices => 'Service notices';

Expand Down
24 changes: 24 additions & 0 deletions lib/l10n/app_localizations_zh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3031,6 +3031,18 @@ class AppLocalizationsZh extends AppLocalizations {
return '往 $destination';
}

@override
String get metroLocateNearest => '定位最近的車站';

@override
String get metroLocating => '正在尋找最近的車站…';

@override
String get metroNearbyPartial => '部分捷運系統暫時無法查詢,已選擇目前可取得資料中最近的車站。';

@override
String get metroStationLookupFailed => '無法載入捷運站資料,請稍後再試。';

@override
String get railOperatingNotices => '營運公告';

Expand Down Expand Up @@ -6363,6 +6375,18 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
return '往 $destination';
}

@override
String get metroLocateNearest => '定位最近的車站';

@override
String get metroLocating => '正在尋找最近的車站…';

@override
String get metroNearbyPartial => '部分捷運系統暫時無法查詢,已選擇目前可取得資料中最近的車站。';

@override
String get metroStationLookupFailed => '無法載入捷運站資料,請稍後再試。';

@override
String get railOperatingNotices => '營運公告';

Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"@@locale": "zh",
"metroLocateNearest": "定位最近的車站",
"metroLocating": "正在尋找最近的車站…",
"metroNearbyPartial": "部分捷運系統暫時無法查詢,已選擇目前可取得資料中最近的車站。",
"metroStationLookupFailed": "無法載入捷運站資料,請稍後再試。",
"appTitle": "YetAnotherBusApp",
"settingsAppearanceDescription": "語言、主題、介面大小與個人化",
"settingsUsageDescription": "到站資訊更新、智慧推薦與使用偏好",
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_zh_TW.arb
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"@@locale": "zh_TW",
"metroLocateNearest": "定位最近的車站",
"metroLocating": "正在尋找最近的車站…",
"metroNearbyPartial": "部分捷運系統暫時無法查詢,已選擇目前可取得資料中最近的車站。",
"metroStationLookupFailed": "無法載入捷運站資料,請稍後再試。",
"appTitle": "YetAnotherBusApp",
"settingsAppearanceDescription": "語言、主題、介面大小與個人化",
"settingsUsageDescription": "到站資訊更新、智慧推薦與使用偏好",
Expand Down
Loading
Loading