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
Binary file added assets/map/icons/cross.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-1-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-2-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-3-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-4-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-5-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-6-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-7-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-8-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-9-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/map/icons/intensity-9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions lib/app/router/app_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:dpip/app/shell/main_shell.dart';
import 'package:dpip/core/settings/onboarding_store.dart';
import 'package:dpip/features/data/presentation/pages/data_page.dart';
import 'package:dpip/features/earthquake/presentation/pages/earthquake_page.dart';
import 'package:dpip/features/earthquake/presentation/pages/report_detail_page.dart';
import 'package:dpip/features/earthquake/presentation/pages/report_list_page.dart';
import 'package:dpip/features/events/presentation/pages/events_page.dart';
import 'package:dpip/features/home/presentation/pages/home_page.dart';
Expand Down Expand Up @@ -71,6 +72,15 @@ final GoRouter appRouter = GoRouter(
path: AppRoutes.earthquakePath,
name: AppRoutes.earthquake,
builder: (_, _) => const ReportListPage(),
routes: [
GoRoute(
path: AppRoutes.earthquakeReportPath,
name: AppRoutes.earthquakeReport,
builder: (_, state) => ReportDetailPage(
reportId: state.pathParameters['id']!,
),
Comment on lines +79 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[maintainability · low]
在使用 state.pathParameters['id']! 時,使用了強制解包(bang operator !)。雖然在路由配置中 AppRoutes.earthquakeReportPath 定義為 :id,理論上該參數應該存在,但直接使用 ! 可能在參數缺失或路由解析異常時導致運行時錯誤(Runtime Error)。建議考慮更穩健的處理方式,例如提供預設值或進行檢查。

Suggestion:

Suggested change
builder: (_, state) => ReportDetailPage(
reportId: state.pathParameters['id']!,
),
builder: (_, state) => ReportDetailPage(
reportId: state.pathParameters['id'] ?? '',
),

),
],
),
GoRoute(
path: AppRoutes.eewPath,
Expand Down
7 changes: 7 additions & 0 deletions lib/features/earthquake/data/report_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ library;
import 'package:dpip/core/error/result.dart';
import 'package:dpip/core/network/api_exception.dart';
import 'package:dpip/features/earthquake/data/earthquake_api.dart';
import 'package:dpip/features/earthquake/domain/earthquake_report.dart';
import 'package:dpip/features/earthquake/domain/partial_earthquake_report.dart';
import 'package:dpip/features/earthquake/domain/report_list_query.dart';
import 'package:dpip/features/earthquake/domain/report_repository.dart';
Expand Down Expand Up @@ -40,6 +41,12 @@ class ReportRepositoryImpl implements ReportRepository {
return parseReportList(raw);
});

@override
Future<Result<EarthquakeReport>> get(String id) => guardResult(() async {
final raw = await _api.getReport(id);
return EarthquakeReport.fromJson((raw as Map).cast<String, dynamic>());
});
Comment on lines +44 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[maintainability · low]
get 方法中,使用 raw as Map 進行強轉型。若 API 回傳 null 或非 Map 格式,會觸發 TypeError。雖然 guardResult 會捕捉此異常,但建議使用更明確的型別檢查或更穩健的轉換方式,以提高程式碼的預期性與健壯性。


/// Skips malformed rows so one bad record cannot blank the catalogue.
static List<PartialEarthquakeReport> parseReportList(List<dynamic> raw) {
return [
Expand Down
127 changes: 127 additions & 0 deletions lib/features/earthquake/domain/earthquake_report.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/// Full earthquake report — `GET /api/v2/eq/report/{id}`, the per-area/town
/// intensity breakdown behind a catalogue row ([PartialEarthquakeReport]).
library;

import 'package:freezed_annotation/freezed_annotation.dart';

part 'earthquake_report.freezed.dart';
part 'earthquake_report.g.dart';

/// The full earthquake report — epicentre, magnitude, and the per-area/town
/// felt-intensity breakdown. Fetched by id from [ReportRepository.get] when a
/// catalogue row is opened.
@freezed
abstract class EarthquakeReport with _$EarthquakeReport {
const EarthquakeReport._();

const factory EarthquakeReport({
required String id,
@JsonKey(name: 'lon') required double longitude,
@JsonKey(name: 'lat') required double latitude,
@JsonKey(name: 'loc') required String location,
required double depth,
@JsonKey(name: 'mag') required double magnitude,
required Map<String, AreaIntensity> list,

/// Origin time as Unix **milliseconds**.
required int time,
required int trem,
}) = _EarthquakeReport;

factory EarthquakeReport.fromJson(Map<String, dynamic> json) =>
_$EarthquakeReportFromJson(json);

/// Leading CWA serial segment (e.g. `115032` or `115000`).
String get serial => id.split('-').first;

/// `…000` serials are 小區域有感 — no numbered CWA report.
bool get isLocalFelt => serial.endsWith('000');

/// Numbered CWA report id, or null when [isLocalFelt].
String? get number => isLocalFelt ? null : serial;

bool get hasNumber => number != null;

/// Origin time in UTC.
DateTime get originTimeUtc =>
DateTime.fromMillisecondsSinceEpoch(time, isUtc: true);

/// Short place string — prefer the parenthetical CWA locality when present.
String get shortLocation {
final open = location.indexOf('(');
final close = location.indexOf(')');
if (open >= 0 && close > open) {
var inner = location.substring(open + 1, close);
if (inner.startsWith('位於')) inner = inner.substring(2);
return inner.trim();
}
final fang = location.indexOf('方');
if (fang >= 0) return location.substring(0, fang + 1).trim();
return location.trim();
}

/// Highest observed intensity across every area/town in [list].
int get maxIntensity {
var max = 0;
for (final area in list.values) {
for (final town in area.town.values) {
if (town.intensity > max) max = town.intensity;
}
}
return max;
}

/// The official CWA report page for this event.
Uri get reportUrl {
final segments = id.split('-')..removeAt(0);
final magCode = (magnitude * 10).floor();
final numberSuffix = hasNumber ? number!.substring(3) : '';
return Uri.parse(
'https://scweb.cwa.gov.tw/zh-tw/earthquake/details/'
'${segments.join()}$magCode$numberSuffix',
);
}

/// CWA's rendered report image (地震報告圖). The filename is derived from the
/// Taipei-local origin time, magnitude, and (when numbered) the report's
/// serial suffix — CWA doesn't expose this as a field, only as a static path.
Uri get reportImageUrl {
final t = originTimeUtc.add(const Duration(hours: 8)); // Asia/Taipei
final y = t.year.toString();
final mo = t.month.toString().padLeft(2, '0');
final d = t.day.toString().padLeft(2, '0');
final h = t.hour.toString().padLeft(2, '0');
final mi = t.minute.toString().padLeft(2, '0');
final s = t.second.toString().padLeft(2, '0');
final magCode = (magnitude * 10).floor();
final numberSuffix = hasNumber ? number!.substring(3) : '';
final name = '$y$mo$d$h$mi$s$magCode${numberSuffix}_H.png';
final yearMonth = name.substring(0, 6);
return Uri.parse('https://scweb.cwa.gov.tw/webdata/OLDEQ/$yearMonth/$name');
}
}

/// One area's (縣市) maximum observed intensity and its station/town breakdown.
@freezed
abstract class AreaIntensity with _$AreaIntensity {
const factory AreaIntensity({
@JsonKey(name: 'int') required int intensity,
required Map<String, StationIntensity> town,
}) = _AreaIntensity;

factory AreaIntensity.fromJson(Map<String, dynamic> json) =>
_$AreaIntensityFromJson(json);
}

/// One station/town's observed intensity and coordinates.
@freezed
abstract class StationIntensity with _$StationIntensity {
const factory StationIntensity({
@JsonKey(name: 'lon') required double longitude,
@JsonKey(name: 'lat') required double latitude,
@JsonKey(name: 'int') required int intensity,
}) = _StationIntensity;

factory StationIntensity.fromJson(Map<String, dynamic> json) =>
_$StationIntensityFromJson(json);
}
Loading
Loading