diff --git a/TRViS.IO/Loaders/LoaderJson.cs b/TRViS.IO/Loaders/LoaderJson.cs index 96c67604..f9f19ffa 100644 --- a/TRViS.IO/Loaders/LoaderJson.cs +++ b/TRViS.IO/Loaders/LoaderJson.cs @@ -119,6 +119,8 @@ private LoaderJson(WorkGroupData[] workGroups) TrainNumber = trainData.TrainNumber, WorkId = workId, WorkType = trainData.WorkType, + // TODO: JSONでのNextTrainIdのサポート + NextTrainId = trainIndex != trainList.Length - 1 ? trainList[trainIndex + 1].Id : null }, trainData.TimetableRows.Select((v, i) => new TimetableRow( Id: v.Id ?? i.ToString(), @@ -207,7 +209,8 @@ public void Dispose() IsRideOnMoving: t.IsRideOnMoving, // TODO: E電時刻表用の線色設定のサポート - LineColor_RGB: null + LineColor_RGB: null, + NextTrainId: t.NextTrainId ); } diff --git a/TRViS.IO/Models/DBStructure.cs b/TRViS.IO/Models/DBStructure.cs index 12de4fe9..eac2de1e 100644 --- a/TRViS.IO/Models/DBStructure.cs +++ b/TRViS.IO/Models/DBStructure.cs @@ -324,6 +324,9 @@ public class TrainData : IHasRemarksProperty, IEquatable [Column("color_id")] public int? ColorId { get; set; } + [Column("next_train_id")] + public string? NextTrainId { get; set; } + public bool Equals(TrainData? obj) { if (obj is null) @@ -371,6 +374,8 @@ public bool Equals(TrainData? obj) IsRideOnMoving == obj.IsRideOnMoving && ColorId == obj.ColorId + && + NextTrainId == obj.NextTrainId ); } @@ -401,11 +406,12 @@ public override int GetHashCode() hashCode.Add(DayCount); hashCode.Add(IsRideOnMoving); hashCode.Add(ColorId); + hashCode.Add(NextTrainId); return hashCode.ToHashCode(); } public override string ToString() - => $"TrainData[{WorkId} / {Id}](TrainNumber='{TrainNumber}', MaxSpeed='{MaxSpeed}', SpeedType='{SpeedType}', NominalTractiveCapacity='{NominalTractiveCapacity}', CarCount={CarCount}, Destination='{Destination}', BeginRemarks='{BeginRemarks}', AfterRemarks='{AfterRemarks}', Remarks='{Remarks}', BeforeDeparture='{BeforeDeparture}', TrainInfo='{TrainInfo}', Direction={Direction}, WorkType={WorkType}, AfterArrive='{AfterArrive}', BeforeDeparture_OnStationTrackCol='{BeforeDeparture_OnStationTrackCol}', AfterArrive_OnStationTrackCol='{AfterArrive_OnStationTrackCol}', DayCount={DayCount}, IsRideOnMoving={IsRideOnMoving}, ColorId={ColorId})"; + => $"TrainData[{WorkId} / {Id}](TrainNumber='{TrainNumber}', MaxSpeed='{MaxSpeed}', SpeedType='{SpeedType}', NominalTractiveCapacity='{NominalTractiveCapacity}', CarCount={CarCount}, Destination='{Destination}', BeginRemarks='{BeginRemarks}', AfterRemarks='{AfterRemarks}', Remarks='{Remarks}', BeforeDeparture='{BeforeDeparture}', TrainInfo='{TrainInfo}', Direction={Direction}, WorkType={WorkType}, AfterArrive='{AfterArrive}', BeforeDeparture_OnStationTrackCol='{BeforeDeparture_OnStationTrackCol}', AfterArrive_OnStationTrackCol='{AfterArrive_OnStationTrackCol}', DayCount={DayCount}, IsRideOnMoving={IsRideOnMoving}, ColorId={ColorId}, NextTrainId={NextTrainId})"; } [Table("timetable_row")] diff --git a/TRViS.IO/Models/TrainData.cs b/TRViS.IO/Models/TrainData.cs index 323ebd9d..ab3693e3 100644 --- a/TRViS.IO/Models/TrainData.cs +++ b/TRViS.IO/Models/TrainData.cs @@ -22,5 +22,6 @@ public record TrainData( string? AfterArriveOnStationTrackCol = null, int DayCount = 0, bool? IsRideOnMoving = null, - int? LineColor_RGB = null + int? LineColor_RGB = null, + string? NextTrainId = null ) : IHasRemarksProperty; diff --git a/TRViS.NetworkSyncService/HttpDataProvider.cs b/TRViS.NetworkSyncService/HttpDataProvider.cs index a0f2560d..b3ea5244 100644 --- a/TRViS.NetworkSyncService/HttpDataProvider.cs +++ b/TRViS.NetworkSyncService/HttpDataProvider.cs @@ -95,12 +95,13 @@ void UpdateNextUri() public async Task GetSyncedDataAsync(CancellationToken token) { using HttpResponseMessage response = await _HttpClient.GetAsync(nextUri, token); + System.Diagnostics.Debug.WriteLine($"Uri: {nextUri}"); // 接続に失敗等しない限り、成功として扱う // (ログ出力は今後検討) if (!response.IsSuccessStatusCode) { return new( - Location_m: 0, + Location_m: double.NaN, Time_ms: (long)DateTime.Now.TimeOfDay.TotalMilliseconds, CanStart: false ); @@ -110,16 +111,20 @@ public async Task GetSyncedDataAsync(CancellationToken token) if (json is null) { return new( - Location_m: 0, + Location_m: double.NaN, Time_ms: (long)DateTime.Now.TimeOfDay.TotalMilliseconds, CanStart: false ); } JsonElement root = json.RootElement; - double location_m = 0; + double location_m = double.NaN; try { - location_m = root.GetProperty(LOCATION_M_JSON_KEY).GetDouble(); + JsonElement location_m_element = root.GetProperty(LOCATION_M_JSON_KEY); + if (location_m_element.ValueKind == JsonValueKind.Null) + location_m = double.NaN; + else + location_m = location_m_element.GetDouble(); } catch (KeyNotFoundException) {} catch (FormatException) {} diff --git a/TRViS.NetworkSyncService/NetworkSyncService.cs b/TRViS.NetworkSyncService/NetworkSyncService.cs index 2f4f0b3a..3ce7dcbc 100644 --- a/TRViS.NetworkSyncService/NetworkSyncService.cs +++ b/TRViS.NetworkSyncService/NetworkSyncService.cs @@ -89,7 +89,7 @@ public async Task TickAsync(CancellationToken? cancellationToken = null) void UpdateCurrentStationWithLocation(double location_m) { - if (StaLocationInfo is null || !IsEnabled) + if (StaLocationInfo is null || !IsEnabled || double.IsNaN(location_m)) return; bool isIn(double threshold1, double threshold2) diff --git a/TRViS/AppShell.xaml.cs b/TRViS/AppShell.xaml.cs index e04a17bd..d1832af9 100644 --- a/TRViS/AppShell.xaml.cs +++ b/TRViS/AppShell.xaml.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using TRViS.RootPages; using TRViS.ViewModels; +using TRViS.IO.RequestInfo; namespace TRViS; @@ -54,6 +55,13 @@ public AppShell() InstanceManager.AppViewModel.WindowHeight = DeviceDisplay.Current.MainDisplayInfo.Height; logger.Trace("Display Width/Height: {0}x{1}", InstanceManager.AppViewModel.WindowWidth, InstanceManager.AppViewModel.WindowHeight); + Task.Run(() => InstanceManager.AppViewModel.HandleAppLinkUriAsync(new AppLinkInfo( + AppLinkInfo.FileType.Json, + new(1,0,0), + ResourceUri: new("http://twr.railway-fan-club.com/api/v1/trvis/timetable.json"), + RealtimeServiceUri: new("http://twr.railway-fan-club.com/api/v1/trvis/state") + ), CancellationToken.None)); + logger.Trace("AppShell Created"); } diff --git a/TRViS/Controls/LogView.cs b/TRViS/Controls/LogView.cs deleted file mode 100644 index 296481b4..00000000 --- a/TRViS/Controls/LogView.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Collections.Concurrent; -using System.Text; -using DependencyPropertyGenerator; -using Microsoft.AppCenter.Crashes; - -namespace TRViS.Controls; - -[DependencyProperty( - "PriorityFilter", - DefaultValue = Priority.Error | Priority.Warn -)] -public partial class LogView : ScrollView -{ - [Flags] - public enum Priority - { - Debug = 1 << 0, - Info = 1 << 1, - Warn = 1 << 2, - Error = 1 << 3, - } - - public class Log - { - public DateTime UTCTime { get; } - public Priority Priority { get; } - public string Text { get; } = string.Empty; - - public Log(Priority priority, string text) - { - this.UTCTime = DateTime.UtcNow; - this.Priority = priority; - this.Text = text; - } - public Log(string text) : this(Priority.Info, text) { } - - public override bool Equals(object? obj) - => obj is Log v - && this.UTCTime == v.UTCTime - && this.Priority == v.Priority - && this.Text == v.Text - ; - - public override int GetHashCode() - => this.UTCTime.GetHashCode() - ^ this.Priority.GetHashCode() - ^ this.Text.GetHashCode() - ; - - public override string ToString() - => $"{this.UTCTime:O}\t[{this.Priority}]: {this.Text}"; - } - - static public EventHandler? LogAdded; - static ConcurrentBag _Logs { get; } = new(); - static public IReadOnlyCollection Logs => _Logs; - static public void Add(Log log) - { - try - { - _Logs.Add(log); - LogAdded?.Invoke(_Logs, log); - } - catch (Exception ex) - { - Crashes.TrackError(ex); - } - } - static public void Add(Priority priority, string text) - => Add(new Log(priority, text)); - static public void Add(string text) - => Add(new Log(text)); - - readonly Label label = new(); - StringBuilder builder = new(); - readonly object buildberLock = new(); - - public LogView() - { - this.Content = label; - LogAdded += OnLogAdded; - } - - void OnLogAdded(object? sender, Log log) - { - if (!PriorityFilter.HasFlag(log.Priority)) - return; - - lock (buildberLock) - { - builder.AppendLine(log.ToString()); - } - - updateLabelText(); - } - - partial void OnPriorityFilterChanged() - => Reload(); - - public void Reload() - { - try - { - lock (buildberLock) - { - builder.Clear(); - label.Text = string.Empty; - - Priority priority = this.PriorityFilter; - if (priority != 0) - builder.AppendJoin('\n', _Logs.Where(v => priority.HasFlag(v.Priority))); - } - - updateLabelText(); - } - catch (Exception ex) - { - Crashes.TrackError(ex); - Utils.ExitWithAlert(ex); - } - } - - void updateLabelText() - => MainThread.BeginInvokeOnMainThread(() => label.Text = builder.ToString()); -} diff --git a/TRViS/Controls/ToggleButton.cs b/TRViS/Controls/ToggleButton.cs index 532c640d..041d3e71 100644 --- a/TRViS/Controls/ToggleButton.cs +++ b/TRViS/Controls/ToggleButton.cs @@ -4,6 +4,7 @@ namespace TRViS.Controls; [DependencyProperty("IsChecked", DefaultBindingMode = DefaultBindingMode.TwoWay)] +[DependencyProperty("IsRadio", DefaultBindingMode = DefaultBindingMode.OneWay)] public partial class ToggleButton : ContentView { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); @@ -21,7 +22,7 @@ public ToggleButton() Point? pt = e.GetPosition(this); logger.Debug("Tapped (Pont: {0}, IsEnabled: {1}, IsChecked Before: {2})", pt, IsEnabled, IsChecked); if (IsEnabled) - IsChecked = !IsChecked; + IsChecked = IsRadio || !IsChecked; } catch (Exception ex) { diff --git a/TRViS/DTAC/DTACElementStyles.cs b/TRViS/DTAC/DTACElementStyles.cs index 44e7082d..c17a96f8 100644 --- a/TRViS/DTAC/DTACElementStyles.cs +++ b/TRViS/DTAC/DTACElementStyles.cs @@ -41,6 +41,10 @@ public static readonly AppThemeGenericsBindingExtension MarkerMarkButtonB new(0x00, 0x80, 0x00), new(0x00, 0x80, 0x00) ); + public static readonly AppThemeColorBindingExtension SemiDarkGreen = new( + new(0x00, 0x77, 0x00), + new(0x00, 0x77, 0x00) + ); public static readonly AppThemeColorBindingExtension DarkGreen = new( new(0x00, 0x44, 0x00), new(0x00, 0x33, 0x00) @@ -61,6 +65,8 @@ public static readonly AppThemeGenericsBindingExtension MarkerMarkButtonB public static readonly double DefaultTextSize = 14; public static readonly double DefaultTextSizePlus = 15; public static readonly double LargeTextSize = 24; + public static readonly double TimetableFontSize = DeviceInfo.Current.Platform == DevicePlatform.iOS ? 28 : 26; + public static readonly double TimetableRunLimitFontSize = DeviceInfo.Current.Platform == DevicePlatform.iOS ? 24 : 22; public const int BeforeDeparture_AfterArrive_Height = 45; @@ -165,6 +171,7 @@ public static Style BeforeRemarksStyleResource v.HorizontalOptions = LayoutOptions.Start; v.VerticalOptions = LayoutOptions.Start; v.FontSize = DefaultTextSizePlus; + v.FontAttributes = FontAttributes.Bold; v.LineHeight = DeviceInfo.Platform == DevicePlatform.Android ? 1.0 : 1.6; // LineHeight分だけ上に隙間が空くため、MarginTopは設定しない v.Margin = new(32, 0, 0, 0); @@ -251,7 +258,7 @@ public static Style TimetableLabelStyleResource }; _timetableLabelStyleResource.Setters.Add(Label.TextColorProperty, TimetableTextColor); - _timetableLabelStyleResource.Setters.Add(Label.FontSizeProperty, DeviceInfo.Current.Platform == DevicePlatform.iOS ? 28 : 26); + _timetableLabelStyleResource.Setters.Add(Label.FontSizeProperty, TimetableFontSize); _timetableLabelStyleResource.Setters.Add(Label.FontAttributesProperty, FontAttributes.Bold); _timetableLabelStyleResource.Setters.Add(Label.InputTransparentProperty, true); @@ -263,7 +270,7 @@ public static Style TimetableLabelStyleResource T v = LabelStyle(); TimetableTextColor.Apply(v, Label.TextColorProperty); - v.FontSize = DeviceInfo.Current.Platform == DevicePlatform.iOS ? 28 : 26; + v.FontSize = TimetableFontSize; v.FontAttributes = FontAttributes.Bold; v.InputTransparent = true; @@ -305,7 +312,7 @@ public static Style TimetableLargeNumberLabelStyleResource { T v = TimetableLargeNumberLabel(); - v.FontSize = DeviceInfo.Current.Platform == DevicePlatform.iOS ? 24 : 22; + v.FontSize = TimetableRunLimitFontSize; v.Margin = v.Padding = new(0); v.VerticalOptions = LayoutOptions.Center; diff --git a/TRViS/DTAC/HakoParts/SimpleRow.cs b/TRViS/DTAC/HakoParts/SimpleRow.cs index 322281d2..655a88c8 100644 --- a/TRViS/DTAC/HakoParts/SimpleRow.cs +++ b/TRViS/DTAC/HakoParts/SimpleRow.cs @@ -79,12 +79,13 @@ static Frame GenSelectTrainButtonFrame(Label TrainNumberLabel) return v; } - readonly ToggleButton SelectTrainButton = new(); + readonly ToggleButton SelectTrainButton; static ToggleButton GenSelectTrainButton(Frame SelectTrainButtonFrame, EventHandler> IsSelectedChanged, int rowIndex) { ToggleButton v = new() { Content = SelectTrainButtonFrame, + IsRadio = true, }; Grid.SetColumn(v, 1); @@ -130,26 +131,21 @@ static Line GenRouteLine(int rowIndex) } public TrainData TrainData { get; } - public IO.Models.DB.TrainData DBTrainData { get; } public TimetableRow? FirstRow { get; } = null; public TimetableRow? LastRow { get; } = null; - readonly Grid ParentGrid; - - public SimpleRow(Grid parentGrid, int dataIndex, TrainData TrainData, IO.Models.DB.TrainData DBTrainData) + public SimpleRow(Grid parentGrid, int dataIndex, TrainData TrainData) { logger.Debug("Creating"); this.TrainData = TrainData; - this.DBTrainData = DBTrainData; + if (TrainData.Rows is not null) { FirstRow = TrainData.Rows.FirstOrDefault(v => !v.IsInfoRow); LastRow = TrainData.Rows.LastOrDefault(v => !v.IsInfoRow); } - ParentGrid = parentGrid; - int rowIndex_StaName_SelectBtn = dataIndex * 2; int rowIndex_time = rowIndex_StaName_SelectBtn + 1; @@ -177,7 +173,7 @@ public SimpleRow(Grid parentGrid, int dataIndex, TrainData TrainData, IO.Models. parentGrid.Add(RouteLine); parentGrid.Add(ToTimeLabel); - SelectTrainButton.IsChecked = InstanceManager.AppViewModel.SelectedDBTrainData?.Id == DBTrainData.Id; + SelectTrainButton.IsChecked = InstanceManager.AppViewModel.SelectedTrainData?.Id == TrainData.Id; SetTrainNumberButtonState(); logger.Debug("Created"); diff --git a/TRViS/DTAC/HakoParts/SimpleView.cs b/TRViS/DTAC/HakoParts/SimpleView.cs index ba138ad6..993af5a6 100644 --- a/TRViS/DTAC/HakoParts/SimpleView.cs +++ b/TRViS/DTAC/HakoParts/SimpleView.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using Microsoft.AppCenter.Crashes; using TRViS.IO; -using TRViS.IO.Models.DB; +using TRViS.IO.Models; namespace TRViS.DTAC.HakoParts; @@ -12,6 +12,7 @@ public class SimpleView : Grid public const double STA_NAME_TIME_COLUMN_WIDTH = 120; const double TRAIN_NUMBER_ROW_HEIGHT = 72; const double TIME_ROW_HEIGHT = 20; + List Rows { get; } = new(); SimpleRow? _SelectedRow = null; SimpleRow? SelectedRow { @@ -63,26 +64,16 @@ public SimpleView() void OnIsSelectedChanged(object? sender, bool oldValue, bool newValue) { - if (sender is not SimpleRow row) + if (sender is not SimpleRow row || newValue != true) { - logger.Debug("sender is not SimpleRow"); + logger.Debug("sender is not SimpleRow / newValue != true"); return; } - try { - if (newValue) - { - SelectedRow = row; - InstanceManager.AppViewModel.SelectedDBTrainData = row.DBTrainData; - } - else if (SelectedRow == row) - { - SelectedRow = null; - InstanceManager.AppViewModel.SelectedDBTrainData = null; - InstanceManager.AppViewModel.SelectedTrainData = null; - } + SelectedRow = row; + InstanceManager.AppViewModel.SelectedTrainData = row.TrainData; } catch (Exception ex) { @@ -107,8 +98,21 @@ void OnAppViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) Utils.ExitWithAlert(ex); } } + else if (e.PropertyName == nameof(InstanceManager.AppViewModel.SelectedTrainData)) + { + try + { + OnSelectedTrainChanged(InstanceManager.AppViewModel.SelectedTrainData); + } + catch (Exception ex) + { + logger.Fatal(ex, "Unknown Exception"); + Crashes.TrackError(ex); + Utils.ExitWithAlert(ex); + } + } } - void OnSelectedWorkChanged(Work? newWork) + void OnSelectedWorkChanged(IO.Models.DB.Work? newWork) { logger.Debug("newWork: {0}", newWork?.Name ?? "null"); Clear(); @@ -126,22 +130,24 @@ void OnSelectedWorkChanged(Work? newWork) return; } - IReadOnlyList trainDataList = loader.GetTrainDataList(newWork.Id); + Rows.Clear(); + IReadOnlyList trainDataList = loader.GetTrainDataList(newWork.Id); SetRowDefinitions(trainDataList.Count); - TrainData? selectedDBTrainData = InstanceManager.AppViewModel.SelectedDBTrainData; + TrainData? selectedTrainData = InstanceManager.AppViewModel.SelectedTrainData; for (int i = 0; i < trainDataList.Count; i++) { - TrainData dbTrainData = trainDataList[i]; - IO.Models.TrainData? trainData = loader.GetTrainData(dbTrainData.Id); + string trainId = trainDataList[i].Id; + IO.Models.TrainData? trainData = loader.GetTrainData(trainId); if (trainData is null) { logger.Debug("trainData is null"); continue; } - SimpleRow row = new(this, i, trainData, dbTrainData); + SimpleRow row = new(this, i, trainData); + Rows.Add(row); row.IsSelectedChanged += OnIsSelectedChanged; - if (dbTrainData == selectedDBTrainData) + if (trainId == selectedTrainData?.Id) { logger.Debug("trainData == selectedTrainData ({0})", trainData.TrainNumber); row.IsSelected = true; @@ -150,6 +156,24 @@ void OnSelectedWorkChanged(Work? newWork) } } + void OnSelectedTrainChanged(TrainData? newTrainData) + { + logger.Debug("newTrainData: {0}", newTrainData?.TrainNumber ?? "null"); + if (newTrainData is null) + { + SelectedRow = null; + return; + } + + if (SelectedRow?.TrainData.Id == newTrainData.Id) + { + logger.Debug("SelectedRow.TrainData.Id == newTrainData.Id ({0})", newTrainData.TrainNumber); + return; + } + + SelectedRow = Rows.FirstOrDefault(v => v.TrainData.Id == newTrainData.Id); + } + void SetRowDefinitions(int workCount) { int currentWorkCount = RowDefinitions.Count / 2; diff --git a/TRViS/DTAC/TimetableParts/NextTrainButton.cs b/TRViS/DTAC/TimetableParts/NextTrainButton.cs new file mode 100644 index 00000000..302f6160 --- /dev/null +++ b/TRViS/DTAC/TimetableParts/NextTrainButton.cs @@ -0,0 +1,66 @@ +using TRViS.IO.Models; +using TRViS.ValueConverters.DTAC; + +namespace TRViS.DTAC.TimetableParts; + +public class NextTrainButton : Grid +{ + readonly Button _NextTrainButton = new() + { + FontFamily = DTACElementStyles.DefaultFontFamily, + FontSize = DTACElementStyles.LargeTextSize, + TextColor = Colors.White, + FontAttributes = FontAttributes.Bold, + Margin = new(32, 8), + MinimumWidthRequest = 400, + CornerRadius = 4, + Shadow = DTACElementStyles.DefaultShadow, + FontAutoScalingEnabled = false, + }; + + public NextTrainButton() + { + DTACElementStyles.SemiDarkGreen.Apply(_NextTrainButton, BackgroundColorProperty); + _NextTrainButton.Clicked += NextTrainButton_Click; + + ColumnDefinitions.Add(new(new(1, GridUnitType.Star))); + ColumnDefinitions.Add(new(new(4, GridUnitType.Star))); + ColumnDefinitions.Add(new(new(1, GridUnitType.Star))); + Grid.SetColumn(_NextTrainButton, 1); + Children.Add(_NextTrainButton); + } + + private string _NextTrainId = string.Empty; + public string NextTrainId + { + get => _NextTrainId; + set + { + if (_NextTrainId == value) + return; + + TrainData? nextTrainData = InstanceManager.AppViewModel.Loader?.GetTrainData(value); + if (nextTrainData is null) + { + throw new KeyNotFoundException($"Next TrainData not found (id: {value})"); + } + else if (nextTrainData.TrainNumber is null) + { + throw new NullReferenceException($"Next TrainData has no TrainNumber (id: {value})"); + } + + _NextTrainId = value; + + string trainNumberToShow = Utils.InsertCharBetweenCharAndMakeWide(nextTrainData.TrainNumber, Utils.THIN_SPACE); + _NextTrainButton.Text = $"{trainNumberToShow}の時刻表へ"; + } + } + + private void NextTrainButton_Click(object? _, EventArgs e) + { + if (string.IsNullOrEmpty(_NextTrainId)) + return; + + InstanceManager.AppViewModel.SelectedTrainData = InstanceManager.AppViewModel.Loader?.GetTrainData(_NextTrainId); + } +} diff --git a/TRViS/DTAC/TimetableParts/VerticalTimetableView.Init.cs b/TRViS/DTAC/TimetableParts/VerticalTimetableView.Init.cs index 9c5b2096..607a90e9 100644 --- a/TRViS/DTAC/TimetableParts/VerticalTimetableView.Init.cs +++ b/TRViS/DTAC/TimetableParts/VerticalTimetableView.Init.cs @@ -2,6 +2,7 @@ using Microsoft.Maui.Controls.Shapes; using TRViS.Controls; +using TRViS.DTAC.TimetableParts; using TRViS.IO.Models; using TRViS.Services; @@ -35,6 +36,7 @@ public partial class VerticalTimetableView readonly AfterRemarks AfterRemarks; readonly BeforeDeparture_AfterArrive AfterArrive; + readonly NextTrainButton NextTrainButton = new(); readonly List RowViewList = new(); @@ -45,6 +47,8 @@ public VerticalTimetableView() AfterArrive = new(this, "着後"); AfterRemarks = new(this); + Grid.SetColumnSpan(NextTrainButton, 8); + ColumnDefinitions = DTACElementStyles.TimetableColumnWidthCollection; Grid.SetColumnSpan(CurrentLocationLine, 8); @@ -145,16 +149,22 @@ await MainThread.InvokeOnMainThreadAsync(() => logger.Trace("ClearOldRowViews Task Complete"); int newCount = newValue?.Length ?? 0; - logger.Debug("newCount: {0}", newCount); + bool hasAfterArrive = trainData?.AfterArrive is not null; + bool hasNextTrainButton = trainData?.NextTrainId is not null; + logger.Debug("newCount: {0}, hasAfterArrive: {1}, hasNextTrainButton: {2}", newCount, hasAfterArrive, hasNextTrainButton); try { RowsCount = newCount; - SetRowDefinitions(newCount); + SetRowDefinitions(newCount, hasAfterArrive, hasNextTrainButton); await AddSeparatorLines(); AfterRemarks.SetRow(newCount); AfterArrive.SetRow(newCount + 1); + if (hasAfterArrive) + Grid.SetRow(NextTrainButton, newCount + 2); + else + Grid.SetRow(NextTrainButton, newCount + 1); logger.Trace("Starting RowViewInit Task..."); } @@ -198,12 +208,25 @@ await MainThread.InvokeOnMainThreadAsync(() => { logger.Trace("MainThread: Inserting Footer..."); - AfterRemarks.Text = trainData?.AfterRemarks ?? string.Empty; - AfterArrive.Text = trainData?.AfterArrive ?? string.Empty; - AfterArrive.Text_OnStationTrackColumn = trainData?.AfterArriveOnStationTrackCol ?? string.Empty; + if (trainData?.AfterRemarks is not null) + { + AfterRemarks.Text = trainData.AfterRemarks; + AfterRemarks.AddToParent(); + } - AfterRemarks.AddToParent(); - AfterArrive.AddToParent(); + if (trainData?.AfterArrive is not null) + { + AfterArrive.Text = trainData.AfterArrive; + AfterArrive.Text_OnStationTrackColumn = trainData.AfterArriveOnStationTrackCol ?? string.Empty; + AfterArrive.AddToParent(); + } + + if (trainData?.NextTrainId is not null) + { + NextTrainButton.NextTrainId = trainData.NextTrainId; + this.Children.Add(NextTrainButton); + } + logger.Trace("NextTrainId: {0}", trainData?.NextTrainId); IsBusy = false; @@ -273,7 +296,7 @@ Task AddNewRow(TimetableRow? row, int index, bool isLastRow) }); } - void SetRowDefinitions(int newCount) + void SetRowDefinitions(int newCount, bool hasAfterArrive = false, bool hasNextTrainButton = false) { int currentCount = RowDefinitions.Count; logger.Debug("Count {0} -> {1}", currentCount, newCount); @@ -283,8 +306,12 @@ void SetRowDefinitions(int newCount) if (DeviceInfo.Current.Idiom == DeviceIdiom.Phone || DeviceInfo.Current.Idiom == DeviceIdiom.Unknown) { - // AfterRemarks & AfterArrive - newCount += 2; + // AfterRemarks + newCount += 1; + if (hasAfterArrive) + newCount += 1; + if (hasNextTrainButton) + newCount += 1; } else { diff --git a/TRViS/DTAC/TimetableParts/WorkAffix.cs b/TRViS/DTAC/TimetableParts/WorkAffix.cs index 52016b6f..f5058473 100644 --- a/TRViS/DTAC/TimetableParts/WorkAffix.cs +++ b/TRViS/DTAC/TimetableParts/WorkAffix.cs @@ -11,14 +11,6 @@ public WorkAffix() BackgroundColor = Colors.White; - LogView logView = new() - { - PriorityFilter - = LogView.Priority.Info - | LogView.Priority.Warn - | LogView.Priority.Error - }; - Content = logView; DTACElementStyles.DefaultBGColor.Apply(this, BackgroundColorProperty); logger.Trace("Created"); diff --git a/TRViS/DTAC/VerticalStylePage.xaml.cs b/TRViS/DTAC/VerticalStylePage.xaml.cs index dc6c2f49..cfc5ee32 100644 --- a/TRViS/DTAC/VerticalStylePage.xaml.cs +++ b/TRViS/DTAC/VerticalStylePage.xaml.cs @@ -151,6 +151,13 @@ public VerticalStylePage() TimetableView.CanUseLocationServiceChanged += (_, canUseLocationService) => { logger.Info("CanUseLocationServiceChanged: {0}", canUseLocationService); PageHeaderArea.CanUseLocationService = canUseLocationService; + + if (!canUseLocationService && CurrentShowingTrainData?.NextTrainId is not null) + { + logger.Info("Can't use LocationService -> try to load NextTrainData"); + InstanceManager.AppViewModel.SelectedTrainData = InstanceManager.AppViewModel.Loader?.GetTrainData(CurrentShowingTrainData.NextTrainId); + } + PageHeaderArea.IsRunning = PageHeaderArea.IsLocationServiceEnabled = TimetableView.IsLocationServiceEnabled = canUseLocationService; }; PageHeaderArea.CanUseLocationService = TimetableView.CanUseLocationService; @@ -188,6 +195,7 @@ partial void OnSelectedTrainDataChanged(TrainData? newValue) try { + VerticalTimetableView_ScrollRequested(this, new(0)); CurrentShowingTrainData = newValue; logger.Info("SelectedTrainDataChanged: {0}", newValue); BindingContext = newValue; diff --git a/TRViS/DTAC/ViewHost.xaml b/TRViS/DTAC/ViewHost.xaml index 1b213343..7285ee01 100644 --- a/TRViS/DTAC/ViewHost.xaml +++ b/TRViS/DTAC/ViewHost.xaml @@ -96,6 +96,7 @@ TargetMode="VerticalView"/> diff --git a/TRViS/Platforms/MacCatalyst/Info.plist b/TRViS/Platforms/MacCatalyst/Info.plist index 63061032..2ee91d99 100644 --- a/TRViS/Platforms/MacCatalyst/Info.plist +++ b/TRViS/Platforms/MacCatalyst/Info.plist @@ -44,5 +44,10 @@ Copyright (c) 2023 Tetsu Otter LSApplicationCategoryType public.app-category.utilities + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/TRViS/Platforms/iOS/Info.plist b/TRViS/Platforms/iOS/Info.plist index 13acda85..3acf08e6 100644 --- a/TRViS/Platforms/iOS/Info.plist +++ b/TRViS/Platforms/iOS/Info.plist @@ -47,5 +47,10 @@ NSLocationWhenInUseUsageDescription Check whether you are near a station or not. + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/TRViS/RootPages/SelectTrainPage.xaml b/TRViS/RootPages/SelectTrainPage.xaml index 6aab8b16..70370201 100644 --- a/TRViS/RootPages/SelectTrainPage.xaml +++ b/TRViS/RootPages/SelectTrainPage.xaml @@ -15,11 +15,10 @@ - + - @@ -61,7 +60,6 @@ Grid.Row="2"> @@ -116,33 +114,6 @@ - - - - - - - - - diff --git a/TRViS/Services/LocationService.Gps.cs b/TRViS/Services/LocationService.Gps.cs index f7895781..0faae13c 100644 --- a/TRViS/Services/LocationService.Gps.cs +++ b/TRViS/Services/LocationService.Gps.cs @@ -21,7 +21,6 @@ async Task GpsPositioningTask(ILocationService service, CancellationToken token) GeolocationRequest req = new(GeolocationAccuracy.Default, Interval); logger.Info("Starting Location Service... (Interval: {0})", Interval); - LogView.Add("Location Service Starting..."); bool isFirst = true; while (!token.IsCancellationRequested) { @@ -45,7 +44,6 @@ async Task GpsPositioningTask(ILocationService service, CancellationToken token) logger.Error(ex, "Location Service Request Permission Failed"); IsEnabled = false; serviceCancellation?.Cancel(); - LogView.Add(LogView.Priority.Error, "Location Service Request Permission Failed:" + ex.ToString()); if (ExceptionThrown is null) throw; @@ -77,7 +75,6 @@ async Task GpsPositioningTask(ILocationService service, CancellationToken token) logger.Error(ex, "GetLocationAsync failed"); IsEnabled = false; serviceCancellation?.Cancel(); - LogView.Add(LogView.Priority.Error, "GetLocationAsync failed:" + ex.ToString()); if (ExceptionThrown is null) throw; @@ -91,14 +88,12 @@ async Task GpsPositioningTask(ILocationService service, CancellationToken token) if (isFirst) { logger.Info("Location Service First Positioning"); - LogView.Add($"Location Service Started with lonlat: ({loc.Longitude}, {loc.Latitude})"); isFirst = false; gpsService.ForceSetLocationInfo(loc.Longitude, loc.Latitude); } else { double distance = gpsService.SetCurrentLocation(loc.Longitude, loc.Latitude); - LogView.Add($"lonlat: ({loc.Longitude}, {loc.Latitude}), distance: {distance}m"); if (double.IsNaN(distance)) { IsEnabled = false; @@ -109,7 +104,6 @@ async Task GpsPositioningTask(ILocationService service, CancellationToken token) else { logger.Warn("Location Service Positioning Failed"); - LogView.Add("Location Service Positioning Failed"); } if (token.IsCancellationRequested) @@ -130,6 +124,5 @@ async Task GpsPositioningTask(ILocationService service, CancellationToken token) } logger.Info("Location Service Ended"); - LogView.Add("Location Service Ended"); } } diff --git a/TRViS/Services/LocationService.Network.cs b/TRViS/Services/LocationService.Network.cs index c6cdb813..066339ee 100644 --- a/TRViS/Services/LocationService.Network.cs +++ b/TRViS/Services/LocationService.Network.cs @@ -4,8 +4,10 @@ namespace TRViS.Services; public partial class LocationService { + const int EXCEPTION_MAX = 10; async Task NetworkSyncServiceTask(NetworkSyncService networkService, CancellationToken token) { + int exceptionCounter = 0; while (!token.IsCancellationRequested) { logger.Trace("NetworkSyncServiceTask Loop"); @@ -23,26 +25,46 @@ await Task.WhenAll( // Network経由の場合は負荷が少ないため、頻度が高くても問題ないはず Task.Delay(Interval / 5, token) ); + exceptionCounter = 0; } - catch (TaskCanceledException) + catch (TaskCanceledException exTask) { - logger.Debug("Task is canceled -> break"); - break; + if (token.IsCancellationRequested) + { + logger.Debug("Cancellation is requested -> break"); + break; + } + + logger.Debug("Task is canceled -> treat as exception"); + if (exceptionCounter++ <= EXCEPTION_MAX) + { + logger.Warn("Exception Counter: {0}", exceptionCounter); + await Task.Delay(Interval, token); + continue; + } + + IsEnabled = false; + serviceCancellation?.Cancel(); + logger.Warn("Switching to GpsPositioningTask"); + SetLonLatLocationService(); + ExceptionThrown?.Invoke(this, exTask); + return; } catch (Exception ex) { logger.Error(ex, "NetworkSyncServiceTask Loop Failed"); + + if (exceptionCounter++ <= EXCEPTION_MAX) + { + logger.Warn("Exception Counter: {0}", exceptionCounter); + await Task.Delay(Interval, token); + continue; + } IsEnabled = false; serviceCancellation?.Cancel(); - LogView.Add(LogView.Priority.Error, "NetworkSyncServiceTask Loop Failed:" + ex.ToString()); - - object? o = _CurrentService; - if (ReferenceEquals(o, networkService)) - SetLonLatLocationService(); - if (ExceptionThrown is null) - throw; - else - ExceptionThrown.Invoke(this, ex); + logger.Warn("Switching to GpsPositioningTask"); + SetLonLatLocationService(); + ExceptionThrown?.Invoke(this, ex); return; } } diff --git a/TRViS/Services/LocationService.cs b/TRViS/Services/LocationService.cs index 9867f7f4..3cba6f51 100644 --- a/TRViS/Services/LocationService.cs +++ b/TRViS/Services/LocationService.cs @@ -36,7 +36,7 @@ public bool IsEnabled public event EventHandler? CanUseServiceChanged; - public event EventHandler LocationStateChanged; + public event EventHandler? LocationStateChanged; public event EventHandler? TimeChanged; public event EventHandler? ExceptionThrown; @@ -52,13 +52,7 @@ public LocationService() logger.Trace("Creating..."); IsEnabled = false; - SetLonLatLocationService(); - - LocationStateChanged += (sender, e) => - { - StaLocationInfo? newStaLocationInfo = _CurrentService?.StaLocationInfo?.ElementAtOrDefault(e.NewStationIndex); - LogView.Add($"LocationStateChanged: Station[{e.NewStationIndex}]@({newStaLocationInfo?.Location_lon_deg}, {newStaLocationInfo?.Location_lat_deg} & Radius:{newStaLocationInfo?.NearbyRadius_m}) IsRunningToNextStation:{e.IsRunningToNextStation}"); - }; + SetNetworkSyncServiceAsync(new Uri("http://twr.railway-fan-club.com/api/v1/trvis/state")); logger.Debug("LocationService is created"); } @@ -123,8 +117,8 @@ void OnAppViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) case nameof(AppViewModel.SelectedWork): networkSyncService.WorkId = InstanceManager.AppViewModel.SelectedWork?.Id; break; - case nameof(AppViewModel.SelectedDBTrainData): - networkSyncService.TrainId = InstanceManager.AppViewModel.SelectedDBTrainData?.Id; + case nameof(AppViewModel.SelectedTrainData): + networkSyncService.TrainId = InstanceManager.AppViewModel.SelectedTrainData?.Id; break; } } @@ -173,7 +167,7 @@ public void SetLonLatLocationService() int lastTime_s = -1; while (!nextTokenSource.Token.IsCancellationRequested) { - logger.Trace("TimeProviderTask Loop"); + // logger.Trace("TimeProviderTask Loop"); if (nextTokenSource.Token.IsCancellationRequested) { @@ -202,7 +196,6 @@ public void SetLonLatLocationService() logger.Error(ex, "TimeProviderTask Loop Failed"); IsEnabled = false; timeProviderCancellation?.Cancel(); - LogView.Add(LogView.Priority.Error, "TimeProviderTask Loop Failed:" + ex.ToString()); if (ExceptionThrown is null) throw; @@ -240,7 +233,7 @@ public async Task SetNetworkSyncServiceAsync(Uri uri, CancellationToken? token = nextService.StaLocationInfo = currentService?.StaLocationInfo; nextService.WorkGroupId = InstanceManager.AppViewModel.SelectedWorkGroup?.Id; nextService.WorkId = InstanceManager.AppViewModel.SelectedWork?.Id; - nextService.TrainId = InstanceManager.AppViewModel.SelectedDBTrainData?.Id; + nextService.TrainId = InstanceManager.AppViewModel.SelectedTrainData?.Id; if (!isIdChangedEventHandlerSet) { logger.Debug("Add EventHandlers for AppViewModel.PropertyChanged"); @@ -249,7 +242,7 @@ public async Task SetNetworkSyncServiceAsync(Uri uri, CancellationToken? token = } _CurrentService = nextService; if (nextService.CanUseService != currentService?.CanUseService) - CanUseServiceChanged?.Invoke(this, nextService.CanUseService); + await MainThread.InvokeOnMainThreadAsync(() => CanUseServiceChanged?.Invoke(this, nextService.CanUseService)); timeProviderCancellation?.Cancel(); timeProviderCancellation?.Dispose(); diff --git a/TRViS/Utils/SampleDataLoader.cs b/TRViS/Utils/SampleDataLoader.cs index c894fd1b..9f3ef84e 100644 --- a/TRViS/Utils/SampleDataLoader.cs +++ b/TRViS/Utils/SampleDataLoader.cs @@ -54,6 +54,8 @@ public class SampleDataLoader : TRViS.IO.ILoader AfterArrive: "入換 20分", AfterArriveOnStationTrackCol: "入換", + NextTrainId: TRAIN_1_1_2, + Rows: new[] { new TimetableRow( @@ -384,6 +386,7 @@ public class SampleDataLoader : TRViS.IO.ILoader Remarks: null, BeforeDeparture: null, TrainInfo: null, + NextTrainId: TRAIN_1_1_3, Rows: new[] { new TimetableRow( @@ -472,7 +475,7 @@ public class SampleDataLoader : TRViS.IO.ILoader Remarks: "記事\n任意の内容" ), }, - 1 + Direction: 1 ); static readonly IO.Models.TrainData SampleTrainData3 = new( diff --git a/TRViS/ValueConverters/DTAC.TrainNumberConverter.cs b/TRViS/ValueConverters/DTAC.TrainNumberConverter.cs index cfa8db84..d918e7ae 100644 --- a/TRViS/ValueConverters/DTAC.TrainNumberConverter.cs +++ b/TRViS/ValueConverters/DTAC.TrainNumberConverter.cs @@ -5,17 +5,19 @@ namespace TRViS.ValueConverters.DTAC; public class TrainNumberConverter : IValueConverter { - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is not string s) - return value; - - return s.Length switch + public static string Convert(in string s) + => s.Length switch { <= 6 => Utils.InsertCharBetweenCharAndMakeWide(s, Utils.SPACE_CHAR), <= 9 => Utils.InsertCharBetweenCharAndMakeWide(s, Utils.THIN_SPACE), _ => s }; + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not string s) + return value; + + return Convert(s); } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) diff --git a/TRViS/ViewModels/AppViewModel.cs b/TRViS/ViewModels/AppViewModel.cs index 80620b2d..13874607 100644 --- a/TRViS/ViewModels/AppViewModel.cs +++ b/TRViS/ViewModels/AppViewModel.cs @@ -21,15 +21,10 @@ public partial class AppViewModel : ObservableObject [ObservableProperty] IReadOnlyList? _WorkList; - [ObservableProperty] - IReadOnlyList? _DBTrainDataList; - [ObservableProperty] TRViS.IO.Models.DB.WorkGroup? _SelectedWorkGroup; [ObservableProperty] TRViS.IO.Models.DB.Work? _SelectedWork; - [ObservableProperty] - TRViS.IO.Models.DB.TrainData? _SelectedDBTrainData; [ObservableProperty] TrainData? _SelectedTrainData; @@ -102,16 +97,18 @@ partial void OnSelectedWorkGroupChanged(TRViS.IO.Models.DB.WorkGroup? value) partial void OnSelectedWorkChanged(IO.Models.DB.Work? value) { - DBTrainDataList = null; - SelectedDBTrainData = null; - + logger.Debug("Work: {0}", value?.Id ?? "null"); if (value is not null) { - DBTrainDataList = Loader?.GetTrainDataList(value.Id); - SelectedDBTrainData = DBTrainDataList?.FirstOrDefault(); + string? trainId = Loader?.GetTrainDataList(value.Id)?.FirstOrDefault()?.Id; + logger.Debug("FirstTrainId: {0}", trainId ?? "null"); + var selectedTrainData = trainId is null ? null : Loader?.GetTrainData(trainId); + SelectedTrainData = selectedTrainData; + logger.Debug("SelectedTrainData: {0} ({1})", SelectedTrainData?.Id ?? "null", selectedTrainData?.Id ?? "null"); + } + else + { + SelectedTrainData = null; } } - - partial void OnSelectedDBTrainDataChanged(IO.Models.DB.TrainData? value) - => SelectedTrainData = value is null ? null : Loader?.GetTrainData(value.Id); }