using Cryville.EEW.BMKGOpenData; using Cryville.EEW.BMKGOpenData.TTS; using Cryville.EEW.Core; using Cryville.EEW.CWAOpenData; using Cryville.EEW.CWAOpenData.Model; using Cryville.EEW.CWAOpenData.TTS; using Cryville.EEW.EMSC; using Cryville.EEW.FANStudio; using Cryville.EEW.FANStudio.TTS; using Cryville.EEW.GeoNet; using Cryville.EEW.GeoNet.TTS; using Cryville.EEW.GlobalQuake; using Cryville.EEW.JMAAtom; using Cryville.EEW.JMAAtom.TTS; using Cryville.EEW.NOAA; using Cryville.EEW.NOAA.TTS; using Cryville.EEW.QuakeML; using Cryville.EEW.Report; using Cryville.EEW.Unity.Map; using Cryville.EEW.Unity.UI; using Cryville.EEW.UpdateChecker; using Cryville.EEW.USGS; using Cryville.EEW.Wolfx; using Cryville.EEW.Wolfx.TTS; using System; using System.Collections.Concurrent; using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using UnityEngine; namespace Cryville.EEW.Unity { sealed class Worker : MonoBehaviour { public static Worker Instance { get; private set; } [SerializeField] CameraController m_cameraController; [SerializeField] MapElementManager m_mapElementManager; [SerializeField] EventOngoingListView m_ongoingEventList; [SerializeField] EventGroupListView m_historyEventGroupList; [SerializeField] GameObject m_connectingHint; CoreWorker _worker; ReportGrouper _grouper; CancellationTokenSource _cancellationTokenSource; void Awake() { if (Instance != null) { Destroy(this); throw new InvalidOperationException("Duplicate worker."); } Instance = this; try { App.Init(); _worker = new(new TTSWorker()); _grouper = new ReportGrouper(); _cancellationTokenSource = new(); } catch (Exception ex) { Dialog.Instance.Show("FATAL ERROR", ex.ToString()); throw; } } void Start() { try { App.MainLogger.Log(1, "App", null, "Initializing localized resources manager"); LocalizedResources.Init(new LocalizedResourcesManager()); RegisterViewModelGenerators(_worker); RegisterTTSMessageGenerators(_worker); BuildWorkers(); _worker.RVMGeneratorContext = SharedSettings.Instance; _worker.TTSMessageGeneratorContext = SharedSettings.Instance; _worker.RVMCulture = SharedSettings.Instance.RVMCulture; _worker.SetTTSCultures(SharedSettings.Instance.TTSCultures ?? new TTSCultureConfig[0]); _worker.IgnoreLanguageVariant = SharedSettings.Instance.DoIgnoreLanguageVariant; _ongoingReportManager.Changed += OnOngoingReported; _worker.Reported += OnReported; _grouper.GroupUpdated += OnGroupUpdated; _grouper.GroupRemoved += OnGroupRemoved; App.MainLogger.Log(1, "App", null, "Worker ready"); Task.Run(() => GatewayVerify(_cancellationTokenSource.Token)).ContinueWith(task => { if (task.IsFaulted) { OnReported(this, new() { Title = task.Exception.Message }); return; } _verified = true; _uiActionQueue.Enqueue(() => m_connectingHint.SetActive(false)); Task.Run(() => ScheduledGatewayVerify(_cancellationTokenSource, _cancellationTokenSource.Token)); Task.Run(() => _worker.RunAsync(_cancellationTokenSource.Token)); Task.Run(() => _ongoingReportManager.RunAsync(_cancellationTokenSource.Token)); }, TaskScheduler.Current); } catch (Exception ex) { Dialog.Instance.Show("FATAL ERROR", ex.ToString()); throw; } } void OnDestroy() { _cancellationTokenSource.Cancel(); _worker.Dispose(); _ongoingReportManager.Dispose(); } CENCEarthquakeRVMGenerator _cencEarthquakeRVMGenerator; void RegisterViewModelGenerators(CoreWorker worker) { worker.RegisterViewModelGenerator(new BMKGEarthquakeRVMGenerator()); worker.RegisterViewModelGenerator(new CEAEEWRVMGenerator()); worker.RegisterViewModelGenerator(_cencEarthquakeRVMGenerator = new CENCEarthquakeRVMGenerator()); worker.RegisterViewModelGenerator(new CENCEEWRVMGenerator()); worker.RegisterViewModelGenerator(new CWAEarthquakeRVMGenerator()); worker.RegisterViewModelGenerator(new CWAEEWRVMGenerator()); worker.RegisterViewModelGenerator(new CWATsunamiRVMGenerator()); worker.RegisterViewModelGenerator(new EMSCRealTimeEventRVMGenerator()); worker.RegisterViewModelGenerator(new FANStudio.FujianEEWRVMGenerator()); worker.RegisterViewModelGenerator(new Wolfx.FujianEEWRVMGenerator()); worker.RegisterViewModelGenerator(new GeoNetQuakeHistoryRVMGenerator()); worker.RegisterViewModelGenerator(new GeoNetQuakeRVMGenerator()); worker.RegisterViewModelGenerator(new GeoNetStrongRVMGenerator()); worker.RegisterViewModelGenerator(new GlobalQuakeRVMGenerator()); worker.RegisterViewModelGenerator(new ICLEEWRVMGenerator()); worker.RegisterViewModelGenerator(new JMAAtomRVMGenerator()); worker.RegisterViewModelGenerator(new JMAEEWRVMGenerator()); worker.RegisterViewModelGenerator(new NOAAAtomRVMGenerator()); var quakemlEventRVMGenerator = new QuakeMLEventRVMGenerator(); quakemlEventRVMGenerator.AddExtension(new USGSQuakeMLExtension()); worker.RegisterViewModelGenerator(quakemlEventRVMGenerator); worker.RegisterViewModelGenerator(new FANStudio.SichuanEEWRVMGenerator()); worker.RegisterViewModelGenerator(new Wolfx.SichuanEEWRVMGenerator()); worker.RegisterViewModelGenerator(new USGSContoursRVMGenerator()); worker.RegisterViewModelGenerator(new VersionRVMGenerator()); } CENCEarthquakeTTSMessageGenerator _cencEarthquakeTTSMessageGenerator; void RegisterTTSMessageGenerators(CoreWorker worker) { worker.RegisterTTSMessageGenerator(new BMKGEarthquakeTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new CEAEEWTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(_cencEarthquakeTTSMessageGenerator = new CENCEarthquakeTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new CENCEEWTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new CWAEarthquakeTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new CWAEEWTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new CWATsunamiTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new FANStudio.TTS.FujianEEWTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new Wolfx.TTS.FujianEEWTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new GeoNetQuakeHistoryTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new GeoNetQuakeTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new GeoNetStrongTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new ICLEEWTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new JMAAtomTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new JMAEEWTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new NOAATTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new FANStudio.TTS.SichuanEEWTTSMessageGenerator()); worker.RegisterTTSMessageGenerator(new Wolfx.TTS.SichuanEEWTTSMessageGenerator()); } bool _verified; void BuildWorkers() { App.MainLogger.Log(1, "App", null, "Building workers"); #if false//UNITY_EDITOR _worker.AddWorker(new WolfxWorker(new Uri("ws://localhost:9995/wolfx"))); _worker.AddWorker(new JMAAtomWorker(new Uri("http://localhost:9095/eqvol.xml"))); _worker.AddWorker(new FANStudioWorker(new("ws://localhost:9995/fan/cea"))); _worker.AddWorker(new CWAReportWorker(new Uri("http://localhost:9095/E-A0014-001.json"), "1")); _worker.AddWorker(new CWAReportWorker(new Uri("http://localhost:9095/E-A0015-001.json"), "1")); _worker.AddWorker(new CWAReportWorker(new Uri("http://localhost:9095/E-A0016-001.json"), "1")); BMKGOpenDataWorker bmkgWorker = new(new Uri("http://localhost:9095/autogempa.json")); bmkgWorker.SetDataUris(new Uri[] { new("http://localhost:9095/gempadirasakan.json") }); _worker.AddWorker(bmkgWorker); _worker.AddWorker(new NOAAAtomWorker(new("http://localhost:9095/PAAQAtom.xml"), forceHttps: false)); _worker.AddWorker(new UpdateCheckerWorker(typeof(Worker).Assembly.GetName().Version?.ToString(3) ?? "", "unity")); #else foreach (var source in SharedSettings.Instance.EventSources) { _worker.AddWorker(source switch { BMKGOpenDataEventSourceConfig bmkgOpenData => BuildBMKGOpenDataWorkerUris(new(new("https://data.bmkg.go.id/DataMKG/TEWS/autogempa.json")), bmkgOpenData), CWAOpenDataEventSourceConfig cwaOpenData => cwaOpenData.Subtype switch { "E-A0014-001" => new CWAReportWorker(new("https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0014-001"), cwaOpenData.Token, 1440, 17280), "E-A0015-001" => new CWAReportWorker(new("https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001"), cwaOpenData.Token), "E-A0016-001" => new CWAReportWorker(new("https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0016-001"), cwaOpenData.Token), _ => throw new InvalidOperationException("Unknown CWA open data sub-type."), }, EMSCRealTimeEventSourceConfig => new EMSCRealTimeWorker(new("wss://www.seismicportal.eu/standing_order/websocket")), FANStudioEventSourceConfig fanStudio => fanStudio.Subtype switch { "cea" => new FANStudioWorker(new("wss://websocket.fanstudio.hk/cea")), "sichuan" => new FANStudioWorker(new("wss://websocket.fanstudio.hk/sichuan")), "fujian" => new FANStudioWorker(new("wss://websocket.fanstudio.hk/fujian")), _ => throw new InvalidOperationException("Unknown FAN Studio sub-type."), }, GeoNetEventSourceConfig geoNet => BuildGeoNetWorker(new(new("https://api.geonet.org.nz/quake"), new("https://api.geonet.org.nz/quake/history/index"), new("https://api.geonet.org.nz/intensity/strong/processed/index")), geoNet), GlobalQuakeServer15EventSourceConfig gq => new GlobalQuakeWorker15(gq.Host, gq.Port), GlobalQuakeServerEventSourceConfig gq => new GlobalQuakeWorker(gq.Host, gq.Port), JMAAtomEventSourceConfig jmaAtom => BuildJMAAtomWorkerFilter(new(new("https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml")), jmaAtom), NOAAEventSourceConfig noaaAtom => noaaAtom.Subtype switch { "PAAQ" => new NOAAAtomWorker(new("https://www.tsunami.gov/events/xml/PAAQAtom.xml"), new("https://www.tsunami.gov/"), new("/php/esri.php?e=t", UriKind.Relative), "PAAQ"), "PHEB" => new NOAAAtomWorker(new("https://www.tsunami.gov/events/xml/PHEBAtom.xml"), new("https://www.tsunami.gov/"), new("/php/esri.php?e=t", UriKind.Relative), "PHEB"), _ => throw new InvalidOperationException("Unknown NOAA sub-type."), }, UpdateCheckerEventSourceConfig => new UpdateCheckerWorker(typeof(Worker).Assembly.GetName().Version?.ToString(3) ?? "", "unity"), USGSEventSourceConfig usgs => BuildUSGSWorker(usgs.UseGeoJSONFeeds ? new USGSGeoJSONWorker(new Uri("https://earthquake.usgs.gov/earthquakes/feed/v1.0/geojson.php")) : new USGSQuakeMLWorker(new Uri("https://earthquake.usgs.gov/earthquakes/feed/v1.0/quakeml.php")) , usgs), WolfxEventSourceConfig wolfx => BuildWolfxWorkerFilter(new WolfxWorker(new("wss://ws-api.wolfx.jp/all_eew")), wolfx), _ => throw new InvalidOperationException("Unknown event source type."), }); } #endif } static JMAAtomWorker BuildJMAAtomWorkerFilter(JMAAtomWorker worker, JMAAtomEventSourceConfig config) { if (config.Filter != null) worker.SetFilter(config.Filter); worker.IsFilterWhitelist = config.IsFilterWhitelist; return worker; } WolfxWorker BuildWolfxWorkerFilter(WolfxWorker worker, WolfxEventSourceConfig config) { if (config.Filter != null) worker.SetFilter(config.Filter.Select(i => i switch { "cenc_eew" => typeof(Wolfx.Model.CENCEEW), "cenc_eqlist" => typeof(Wolfx.Model.WolfxEarthquakeList), "cwa_eew" => typeof(Wolfx.Model.CWAEEW), "fj_eew" => typeof(Wolfx.Model.FujianEEW), "jma_eew" => typeof(Wolfx.Model.JMAEEW), "sc_eew" => typeof(Wolfx.Model.SichuanEEW), _ => throw new InvalidOperationException("Unknown Wolfx event type."), })); worker.IsFilterWhitelist = config.IsFilterWhitelist; _cencEarthquakeRVMGenerator.UseRawLocationName = _cencEarthquakeTTSMessageGenerator.UseRawLocationName = config.UseRawCENCLocationName; return worker; } static BMKGOpenDataWorker BuildBMKGOpenDataWorkerUris(BMKGOpenDataWorker worker, BMKGOpenDataEventSourceConfig config) { worker.SetDataUris(config.Subtypes.Select(i => new Uri(string.Format(CultureInfo.InvariantCulture, "https://data.bmkg.go.id/DataMKG/TEWS/{0}.json", i)))); return worker; } static GeoNetWorker BuildGeoNetWorker(GeoNetWorker worker, GeoNetEventSourceConfig pref) { worker.MinimumMMI = pref.MinimumMMI; worker.DoGetFullHistory = pref.DoGetFullHistory; worker.DoGetStrongMotionInfo = pref.DoGetStrongMotionInfo; return worker; } static USGSWorker BuildUSGSWorker(USGSWorker worker, USGSEventSourceConfig config) { worker.SetFeedRelativeUri(new(string.Format(CultureInfo.InvariantCulture, "/earthquakes/feed/v1.0/summary/{0}{1}", config.Subtype, config.UseGeoJSONFeeds ? ".geojson" : ".quakeml"), UriKind.Relative)); if (worker is USGSGeoJSONWorker geojsonWorker) { geojsonWorker.SetFilter( config.Products .Select(i => i.Split('/', 2)) .GroupBy(i => i[0]) .ToDictionary(g => g.Key, g => g.Select(i => i[1])) ); } return worker; } readonly OngoingReportManager _ongoingReportManager = new(); readonly ConcurrentQueue _uiActionQueue = new(); ReportViewModel _latestHistoryReport; void OnReported(object sender, ReportViewModel e) { if (e.Model is Exception && e.Model is not SourceWorkerNetworkException) App.MainLogger.Log(4, "App", null, "Received an error from {0}: {1}", sender.GetType(), e.Model); _grouper.Report(e); _ongoingReportManager.Report(e); _uiActionQueue.Enqueue(() => { if (m_mapElementManager.OngoingCount == 0) { m_mapElementManager.SetSelected(e); m_cameraController.OnMapElementUpdated(); } if (e.InvalidatedTime == null && (!(e.RevisionKey?.IsCancellation ?? false))) { _latestHistoryReport = e; } }); } void OnOngoingReported(ReportViewModel item, CollectionChangeAction action) { if (action == CollectionChangeAction.Add) { _uiActionQueue.Enqueue(() => { if (m_mapElementManager.Add(item)) { m_mapElementManager.SetSelected(null); } m_ongoingEventList.Add(item); }); } else if (action == CollectionChangeAction.Remove) { _uiActionQueue.Enqueue(() => { m_mapElementManager.Remove(item); if (m_mapElementManager.Count == 0 && SharedSettings.Instance.DoSwitchBackToHistory && _latestHistoryReport is not null) { m_mapElementManager.SetSelected(_latestHistoryReport, true); } m_ongoingEventList.Remove(item); }); } } void OnGroupUpdated(object sender, ReportGroup e) { _uiActionQueue.Enqueue(() => m_historyEventGroupList.UpdateGroup(e)); } void OnGroupRemoved(object sender, ReportGroup e) { _uiActionQueue.Enqueue(() => m_historyEventGroupList.RemoveGroup(e)); } public void SetSelected(ReportViewModel e) { m_mapElementManager.SetSelected(e); m_cameraController.OnMapElementUpdated(); } public void SetCurrent(ReportViewModel e) { if (m_mapElementManager.SetCurrent(e)) { m_cameraController.OnMapElementUpdated(); } } void Update() { while (_uiActionQueue.TryDequeue(out var action)) { action(); } } async Task ScheduledGatewayVerify(CancellationTokenSource source, CancellationToken cancellationToken) { Exception lastEx = null; for (int i = 0; i < 8; i++) { await Task.Delay(TimeSpan.FromHours(3), cancellationToken).ConfigureAwait(true); try { await GatewayVerify(cancellationToken).ConfigureAwait(true); i = -1; } catch (HttpRequestException ex) { lastEx = ex; } catch (WebException ex) { lastEx = ex; } catch (InvalidOperationException ex) { lastEx = ex; break; } } if (lastEx != null) { OnReported(this, new() { Title = lastEx.Message }); _verified = false; } source.Cancel(); } #if UNITY_EDITOR #pragma warning disable CS1998 #endif static async Task GatewayVerify(CancellationToken cancellationToken) { #if !UNITY_EDITOR using var client = new HttpClient(); client.DefaultRequestHeaders.UserAgent.ParseAdd(SharedSettings.Instance.UnityUserAgent); using var response = await client.GetAsync(new Uri("https://gateway.cryville.world/?rin=" + SharedSettings.Instance.Id), cancellationToken).ConfigureAwait(true); if (response.StatusCode is >= ((HttpStatusCode)400) and < ((HttpStatusCode)500)) { throw new InvalidOperationException(response.ReasonPhrase); } response.EnsureSuccessStatusCode(); #endif } #if UNITY_EDITOR #pragma warning restore CS1998 #endif } }