feat: Add config

This commit is contained in:
2025-02-18 12:04:38 +08:00
parent 02fc481993
commit 5b3a51150c
12 changed files with 305 additions and 39 deletions

View File

@@ -1,11 +1,10 @@
using Cryville.Common.Font;
using Cryville.Common.Unity.UI;
using Cryville.Crtr;
using Cryville.Culture;
using System.IO;
using System.Text;
using System.Xml.Linq;
using System.Xml;
using System.Xml.Linq;
using UnityEngine;
namespace Cryville.EEW.Unity {

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Cryville.EEW.Unity {
record Config(
string SeverityScheme,
string SeverityColorMapping,
float SeverityColorMappingLuminanceMultiplier,
bool UseContinuousColor,
string ColorScheme,
string OverrideTimeZone,
bool DoDisplayTimeZone,
bool DoSwitchBackToHistory,
IReadOnlyCollection<EventSourceConfig> EventSources
) {
public static Config Default => new(
"Default",
"Default",
1f,
false,
"Default",
null,
true,
true,
new List<EventSourceConfig> {
new JMAAtomEventSourceConfig(),
new UpdateCheckerEventSourceConfig(),
new WolfxEventSourceConfig(),
}
);
}
[JsonPolymorphic(TypeDiscriminatorPropertyName = "Type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
[JsonDerivedType(typeof(CWAOpenDataEventSourceConfig), "CWAOpenData")]
[JsonDerivedType(typeof(GlobalQuakeServerEventSourceConfig), "GlobalQuakeServer")]
[JsonDerivedType(typeof(GlobalQuakeServer15EventSourceConfig), "GlobalQuakeServer15")]
[JsonDerivedType(typeof(JMAAtomEventSourceConfig), "JMAAtom")]
[JsonDerivedType(typeof(NOAAEventSourceConfig), "NOAA")]
[JsonDerivedType(typeof(UpdateCheckerEventSourceConfig), "UpdateChecker")]
[JsonDerivedType(typeof(WolfxEventSourceConfig), "Wolfx")]
abstract record EventSourceConfig();
record CWAOpenDataEventSourceConfig([property: JsonRequired] string Subtype, [property: JsonRequired] string Token) : EventSourceConfig;
record GlobalQuakeServerEventSourceConfig([property: JsonRequired] string Host, int Port = 38000) : EventSourceConfig;
record GlobalQuakeServer15EventSourceConfig(string Host, int Port = 38000) : GlobalQuakeServerEventSourceConfig(Host, Port);
record JMAAtomEventSourceConfig(IReadOnlyCollection<string> Filter = null, bool IsFilterWhitelist = false) : EventSourceConfig;
record NOAAEventSourceConfig([property: JsonRequired] string Subtype) : EventSourceConfig;
record UpdateCheckerEventSourceConfig : EventSourceConfig;
record WolfxEventSourceConfig(IReadOnlyCollection<string> Filter = null, bool IsFilterWhitelist = false) : EventSourceConfig;
[JsonSerializable(typeof(Config))]
[JsonSourceGenerationOptions(WriteIndented = true)]
sealed partial class ConfigSerializationContext : JsonSerializerContext { }
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b478a322f8c49a247b6761de7b5c0e0a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,7 +1,8 @@
using Cryville.Common.Font;
using System.Collections.Generic;
using UnityEngine;
namespace Cryville.Crtr {
namespace Cryville.EEW.Unity {
internal static class PlatformConfig {
#if UNITY_STANDALONE_WIN
public static readonly string Name = "windows";
@@ -11,11 +12,17 @@ namespace Cryville.Crtr {
#error Unknown platform.
#endif
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
#if UNITY_EDITOR_WIN
public static readonly string ConfigPath = Application.persistentDataPath;
#else
public static readonly string ConfigPath = Application.dataPath;
#endif
public static readonly string FileProtocolPrefix = "file:///";
public static readonly FontManager FontManager = new FontManagerWindows();
public static Dictionary<string, List<string>> ScriptFontMap => FallbackListFontMatcher.GetDefaultWindowsFallbackMap();
public static readonly string TextShader = "TextMesh Pro/Shaders/TMP_SDF SSD";
#elif UNITY_ANDROID
public static readonly string ConfigPath = Application.persistentDataPath;
public static readonly string FileProtocolPrefix = "file://";
public static readonly FontManager FontManager = new FontManagerAndroid();
public static Dictionary<string, List<string>> ScriptFontMap => FallbackListFontMatcher.GetDefaultAndroidFallbackMap();

View File

@@ -5,9 +5,12 @@ using Cryville.EEW.Map;
using Cryville.EEW.Report;
using Cryville.EEW.TTS;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text.Json;
using UnityEngine;
using Color = System.Drawing.Color;
@@ -16,18 +19,25 @@ namespace Cryville.EEW.Unity {
static SharedSettings s_instance;
public static SharedSettings Instance => s_instance ??= new();
public ISeverityScheme SeverityScheme => DefaultSeverityScheme.Instance;
public ISeverityColorMapping SeverityColorMapping => DefaultSeverityColorMapping.Instance;
public bool UseContinuousColor => true;
public IColorScheme ColorScheme => new SeverityBasedColorScheme(SeverityScheme, DefaultSeverityColorMapping.Instance);
public ISubColorScheme BorderColorScheme => new WrappedColorScheme(new SeverityBasedColorScheme(SeverityScheme, DefaultSeverityColorMapping.SecondaryInstance));
public ISubColorScheme TextColorScheme => new DefaultTextColorScheme(Color.White, Color.Black);
public ILocationConverter LocationConverter => new FERegionLongConverter();
public TimeSpan NowcastWarningDelayTolerance => TimeSpan.MaxValue;
public ISeverityScheme SeverityScheme { get; private set; } = DefaultSeverityScheme.Instance;
public ISeverityColorMapping SeverityColorMapping { get; private set; } = DefaultSeverityColorMapping.Instance;
public bool UseContinuousColor { get; private set; } = true;
public IColorScheme ColorScheme { get; private set; } = new SeverityBasedColorScheme(DefaultSeverityScheme.Instance, DefaultSeverityColorMapping.Instance);
public ISubColorScheme BorderColorScheme { get; private set; } = new WrappedColorScheme(new SeverityBasedColorScheme(DefaultSeverityScheme.Instance, DefaultSeverityColorMapping.SecondaryInstance));
public ISubColorScheme TextColorScheme { get; private set; } = new DefaultTextColorScheme(Color.White, Color.Black);
public ILocationConverter LocationConverter => new FERegionLongConverter(); // TODO TTS
public TimeSpan NowcastWarningDelayTolerance => TimeSpan.FromMinutes(60); // TODO TTS
public TimeZoneInfo OverrideTimeZone { get; private set; }
public bool DoDisplayTimeZone { get; private set; } = true;
public bool DoSwitchBackToHistory { get; private set; } = true;
public IReadOnlyCollection<EventSourceConfig> EventSources { get; private set; }
public string Id { get; private set; }
public byte[] IdBytes { get; } = new byte[32];
public string UnityUserAgent { get; private set; }
public void Init() {
Id = SystemInfo.deviceUniqueIdentifier;
using var hash = SHA256.Create();
@@ -44,6 +54,60 @@ namespace Cryville.EEW.Unity {
#else
#error No Unity User Agent
#endif
var file = new FileInfo(Path.Combine(PlatformConfig.ConfigPath, "config.json"));
if (!file.Exists) {
using var stream1 = file.OpenWrite();
using var writer = new StreamWriter(stream1, EEW.SharedSettings.Encoding);
writer.Write(JsonSerializer.Serialize(Config.Default, ConfigSerializationContext.Default.Config));
}
using var stream = file.OpenRead();
var config = JsonSerializer.Deserialize(stream, ConfigSerializationContext.Default.Config) ?? throw new InvalidOperationException("Null config.");
SeverityScheme = config.SeverityScheme switch {
"Default" => new DefaultSeverityScheme(),
"Legacy" => new LegacySeverityScheme(),
_ => throw new InvalidOperationException("Unknown severity scheme."),
};
SeverityColorMapping = config.SeverityColorMapping switch {
"Default" => new DefaultSeverityColorMapping(config.SeverityColorMappingLuminanceMultiplier),
"SREV" => new SREVSeverityColorMapping(config.SeverityColorMappingLuminanceMultiplier),
"SREVBorder" => new SREVBorderSeverityColorMapping(config.SeverityColorMappingLuminanceMultiplier),
_ => throw new InvalidOperationException("Unknown severity color mapping."),
};
UseContinuousColor = config.UseContinuousColor;
ColorScheme = config.ColorScheme switch {
"Default" => new SeverityBasedColorScheme(SeverityScheme, DefaultSeverityColorMapping.Instance),
"SREV" => new SREVColorScheme(),
"DichromaticYB" => new SeverityBasedColorScheme(SeverityScheme, new DichromaticSeverityColorMapping(0.62f, 0.20f, 90)),
"DichromaticRC" => new SeverityBasedColorScheme(SeverityScheme, new DichromaticSeverityColorMapping(0.62f, 0.25f, 30)),
"DichromaticPG" => new SeverityBasedColorScheme(SeverityScheme, new DichromaticSeverityColorMapping(0.62f, 0.30f, -30)),
"Monochromatic" => new SeverityBasedColorScheme(SeverityScheme, new MonochromaticSeverityColorMapping()),
_ => throw new InvalidOperationException("Unknown color scheme."),
};
BorderColorScheme = config.ColorScheme switch {
"Default" => new WrappedColorScheme(new SeverityBasedColorScheme(SeverityScheme, DefaultSeverityColorMapping.SecondaryInstance)),
"SREV" => new WrappedColorScheme(new SREVBorderColorScheme()),
"DichromaticYB" => new WrappedColorScheme(new SeverityBasedColorScheme(SeverityScheme, new DichromaticSeverityColorMapping(0.62f, 0.20f, 90, 0.75f))),
"DichromaticRC" => new WrappedColorScheme(new SeverityBasedColorScheme(SeverityScheme, new DichromaticSeverityColorMapping(0.62f, 0.25f, 30, 0.75f))),
"DichromaticPG" => new WrappedColorScheme(new SeverityBasedColorScheme(SeverityScheme, new DichromaticSeverityColorMapping(0.62f, 0.30f, -30, 0.75f))),
"Monochromatic" => new WrappedColorScheme(new SeverityBasedColorScheme(SeverityScheme, new MonochromaticSeverityColorMapping(0.75f))),
_ => throw new InvalidOperationException("Unknown color scheme."),
};
TextColorScheme = config.ColorScheme switch {
"SREV" => new DefaultTextColorScheme(Color.White, Color.FromArgb(28, 28, 28), 0.555f),
_ => new DefaultTextColorScheme(Color.White, Color.Black),
};
OverrideTimeZone = ParseTimeZone(config.OverrideTimeZone);
DoDisplayTimeZone = config.DoDisplayTimeZone;
DoSwitchBackToHistory = config.DoSwitchBackToHistory;
EventSources = config.EventSources;
}
TimeZoneInfo ParseTimeZone(string timeZone) {
if (timeZone == null) return null;
if (timeZone == "") return TimeZoneInfo.Local;
return TimeZoneInfo.CreateCustomTimeZone("Custom", TimeSpan.Parse(timeZone, CultureInfo.InvariantCulture), null, null);
}
}
}

View File

@@ -39,18 +39,15 @@ namespace Cryville.EEW.Unity.UI {
SetText(m_locationView, location.Value, location.Culture);
SetText(m_predicateView, predicate.Value, predicate.Culture);
if (time.Value is DateTime ttime) {
// TODO
//if (SharedSettings.Instance.OverrideTimeZone is TimeZoneInfo tTimeZone)
// ttime = TimeZoneInfo.ConvertTime(ttime, timeZone, tTimeZone);
//else
// tTimeZone = timeZone;
TimeZoneInfo tTimeZone = timeZone;
if (SharedSettings.Instance.OverrideTimeZone is TimeZoneInfo tTimeZone)
ttime = TimeZoneInfo.ConvertTime(ttime, timeZone, tTimeZone);
else
tTimeZone = timeZone;
if (UseShortTimeFormat) {
SetText(m_timeView, ttime.ToString("G", time.Culture), time.Culture);
}
else {
// TODO SetText(m_timeView, SharedSettings.Instance.DoDisplayTimeZone ? string.Format(time.Culture, "{0:G} ({1})", ttime, tTimeZone.ToTimeZoneString()) : ttime.ToString(time.Culture), time.Culture);
SetText(m_timeView, string.Format(time.Culture, "{0:G} ({1})", ttime, tTimeZone.ToTimeZoneString()), time.Culture);
SetText(m_timeView, SharedSettings.Instance.DoDisplayTimeZone ? string.Format(time.Culture, "{0:G} ({1})", ttime, tTimeZone.ToTimeZoneString()) : ttime.ToString(time.Culture), time.Culture);
}
}
else {

View File

@@ -16,6 +16,8 @@ namespace Cryville.EEW.Unity.UI {
readonly List<ReportViewModel> _displayingReports = new();
readonly List<EventOngoingView> _displayingViews = new();
public int Count => _displayingReports.Count;
public void Add(ReportViewModel e) {
_displayingReports.Add(e);

View File

@@ -13,7 +13,13 @@ namespace Cryville.EEW.Unity.UI {
}
void Update() {
_textView.text = string.Format(CultureInfo.CurrentCulture, "{0:G} ({1})", DateTime.Now, TimeZoneInfo.Local.ToTimeZoneString());
var time = DateTime.Now;
var timeZone = TimeZoneInfo.Local;
if (SharedSettings.Instance.OverrideTimeZone is TimeZoneInfo tTimeZone)
time = TimeZoneInfo.ConvertTime(time, timeZone, tTimeZone);
else
tTimeZone = timeZone;
_textView.text = SharedSettings.Instance.DoDisplayTimeZone ? string.Format(CultureInfo.CurrentCulture, "{0:G} ({1})", time, tTimeZone.ToTimeZoneString()) : time.ToString(CultureInfo.CurrentCulture);
}
}
}

View File

@@ -15,8 +15,8 @@ using Cryville.EEW.Wolfx;
using Cryville.EEW.Wolfx.TTS;
using System;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
@@ -105,37 +105,71 @@ namespace Cryville.EEW.Unity {
}
bool _verified;
WolfxWorker _wolfxWorker;
JMAAtomWorker _jmaWorker;
CWAReportWorker<Tsunami> _cwa14Worker;
CWAReportWorker<Earthquake> _cwa15Worker;
CWAReportWorker<Earthquake> _cwa16Worker;
GlobalQuakeWorker _gqWorker;
NOAAAtomWorker _ntwcWorker;
NOAAAtomWorker _ptwcWorker;
UpdateCheckerWorker _updateChecker;
void BuildWorkers() {
#if UNITY_EDITOR
_worker.AddWorker(_wolfxWorker = new WolfxWorker(new Uri("ws://localhost:9995/wolfx")));
_worker.AddWorker(_jmaWorker = new JMAAtomWorker(new Uri("http://localhost:9095/eqvol.xml")));
_worker.AddWorker(_cwa14Worker = new CWAReportWorker<Tsunami>(new Uri("http://localhost:9095/E-A0014-001.json"), "1"));
_worker.AddWorker(_cwa15Worker = new CWAReportWorker<Earthquake>(new Uri("http://localhost:9095/E-A0015-001.json"), "1"));
_worker.AddWorker(_cwa16Worker = new CWAReportWorker<Earthquake>(new Uri("http://localhost:9095/E-A0016-001.json"), "1"));
_worker.AddWorker(_ntwcWorker = new NOAAAtomWorker(new("http://localhost:9095/PAAQAtom.xml")));
// _worker.AddWorker(_gqWorker = new GlobalQuakeWorker("localhost", 38000));
_worker.AddWorker(new WolfxWorker(new Uri("ws://localhost:9995/wolfx")));
_worker.AddWorker(new JMAAtomWorker(new Uri("http://localhost:9095/eqvol.xml")));
_worker.AddWorker(new CWAReportWorker<Tsunami>(new Uri("http://localhost:9095/E-A0014-001.json"), "1"));
_worker.AddWorker(new CWAReportWorker<Earthquake>(new Uri("http://localhost:9095/E-A0015-001.json"), "1"));
_worker.AddWorker(new CWAReportWorker<Earthquake>(new Uri("http://localhost:9095/E-A0016-001.json"), "1"));
_worker.AddWorker(new NOAAAtomWorker(new("http://localhost:9095/PAAQAtom.xml")));
_worker.AddWorker(new UpdateCheckerWorker(typeof(Worker).Assembly.GetName().Version?.ToString(3) ?? "", "unity"));
#else
// TODO
foreach (var source in SharedSettings.Instance.EventSources) {
_worker.AddWorker(source switch {
CWAOpenDataEventSourceConfig cwaOpenData => cwaOpenData.Subtype switch {
"E-A0014-001" => new CWAReportWorker<Tsunami>(new Uri("https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0014-001"), cwaOpenData.Token, 1440, 17280),
"E-A0015-001" => new CWAReportWorker<Earthquake>(new Uri("https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001"), cwaOpenData.Token),
"E-A0016-001" => new CWAReportWorker<Earthquake>(new Uri("https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0016-001"), cwaOpenData.Token),
_ => throw new InvalidOperationException("Unknown CWA open data sub-type."),
},
GlobalQuakeServer15EventSourceConfig gq => new GlobalQuakeWorker15(gq.Host, gq.Port),
GlobalQuakeServerEventSourceConfig gq => new GlobalQuakeWorker(gq.Host, gq.Port),
JMAAtomEventSourceConfig jmaAtom => BuildJMAAtomWorkerFilter(new JMAAtomWorker(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"),
WolfxEventSourceConfig wolfx => BuildWolfxWorkerFilter(new WolfxWorker(new Uri("wss://ws-api.wolfx.jp/all_eew")), wolfx),
_ => throw new InvalidOperationException("Unknown event source type."),
});
}
#endif
if (_updateChecker == null) _worker.AddWorker(_updateChecker = new(typeof(Worker).Assembly.GetName().Version?.ToString(3) ?? "", "unity"));
}
#if !UNITY_EDITOR
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(CENCEEW),
"cenc_eqlist" => typeof(WolfxEarthquakeList<CENCEarthquake>),
"cwa_eew" => typeof(CWAEEW),
"fj_eew" => typeof(FujianEEW),
"jma_eew" => typeof(JMAEEW),
"sc_eew" => typeof(SichuanEEW),
_ => throw new InvalidOperationException("Unknown Wolfx event type."),
}));
worker.IsFilterWhitelist = config.IsFilterWhitelist;
return worker;
}
#endif
readonly OngoingReportManager _ongoingReportManager = new();
readonly ConcurrentQueue<Action> _uiActionQueue = new();
ReportViewModel _latestHistoryReport;
void OnReported(object sender, ReportViewModel e) {
Debug.Log(e);
_ongoingReportManager.Report(e);
_uiActionQueue.Enqueue(() => {
m_mapElementManager.SetSelected(e);
if (e.InvalidatedTime == null && (!(e.RevisionKey?.IsCancellation ?? false))) {
_latestHistoryReport = e;
}
m_cameraController.OnMapElementUpdated();
});
}
@@ -151,6 +185,9 @@ namespace Cryville.EEW.Unity {
_uiActionQueue.Enqueue(() => {
m_mapElementManager.RemoveOngoing(item);
m_ongoingEventList.Remove(item);
if (m_ongoingEventList.Count == 0 && SharedSettings.Instance.DoSwitchBackToHistory && _latestHistoryReport is not null) {
m_mapElementManager.SetSelected(_latestHistoryReport);
}
m_cameraController.OnMapElementUpdated();
});
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6430b56ba6977f04291fbf3b0a464655
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,78 @@
fileFormatVersion: 2
guid: 15569127b9ff7604198a55092602f59a
labels:
- RoslynAnalyzer
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: None
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant: