48 Commits

Author SHA1 Message Date
a131ed10f9 build: Update project version 2025-07-10 02:17:27 +08:00
ad0797585b ci: Update plugins 2025-07-10 02:15:58 +08:00
304ab10491 fix: Restore debugging event sources 2025-07-10 02:15:47 +08:00
130f880d5d fix: Fix API host of FAN Studio API 2025-07-05 01:43:50 +08:00
cf6d76d4c0 build: Update project version 2025-07-05 01:12:32 +08:00
4628db1d49 feat: Add fatal error dialog 2025-07-05 01:10:15 +08:00
1eeaa4f728 fix: Set the default alpha for multiply material 2025-07-05 01:09:47 +08:00
309ee12b93 style: Code cleanup 2025-07-04 23:49:48 +08:00
59335c9102 feat: Add FAN Studio API event sources 2025-07-04 23:49:32 +08:00
e182ebd53f fix: Update basemap URL template 2025-07-04 23:22:46 +08:00
65b158f387 feat: Implement hillshade layer 2025-07-04 23:22:14 +08:00
9f844f2744 refactor: Update Unity to 2022.3.62 2025-07-04 23:17:17 +08:00
ae917a8e51 ci: Update plugins 2025-07-04 23:15:03 +08:00
44a72d1a8f refactor: Update Unity to 2022.3.62 2025-06-30 16:04:04 +08:00
4758b65159 build: Update project version 2025-06-17 01:16:22 +08:00
de3196d38a fix: Revise the tag of a log point 2025-06-17 01:16:11 +08:00
c2311fb7a4 feat: Add new event sources 2025-06-17 01:15:25 +08:00
7562be2c09 ci: Update plugins 2025-06-17 01:12:55 +08:00
a8f46113d4 build: Update project version 2025-06-06 19:33:15 +08:00
99736f114d style: Code cleanup 2025-06-06 19:32:42 +08:00
4d1a008106 feat: Add logs 2025-06-06 19:32:31 +08:00
9318cbca4e feat: Add new event sources 2025-06-06 19:31:27 +08:00
a162f345c4 ci: Update plugins 2025-06-06 19:30:21 +08:00
1af4afc7c6 build: Update project version 2025-05-07 22:56:45 +08:00
a3efe939e8 fix: Fix TTS COM exception in IL2CPP 2025-05-07 22:56:32 +08:00
5daee1a01a feat: Add TTS related settings 2025-05-06 20:35:52 +08:00
2d5d305528 feat: Implement TTS 2025-05-06 20:35:08 +08:00
8da46c0511 build: Update project version 2025-04-29 19:54:46 +08:00
75b5d7708c fix: Disable force HTTPS in NOAA Atom worker when debugging 2025-04-29 19:54:27 +08:00
5b2177a795 feat: Add using raw location name settings for CENC earthquake reports 2025-04-29 19:54:16 +08:00
8cb33dca5f ci: Update plugins 2025-04-29 19:53:24 +08:00
ae2e0af18a fix: Improve ongoing report round-robin period 2025-04-29 19:52:41 +08:00
2ac5a3d4f0 build: Update project version 2025-04-08 10:01:06 +08:00
5f78a1afde refactor: Code cleanup 2025-04-08 10:00:47 +08:00
ef5cf78a03 feat: Add display culture config 2025-04-08 10:00:28 +08:00
b60e62af70 feat: Adapt to breaking changes 2025-04-08 10:00:01 +08:00
3e59fe1462 feat: Implement region editor 2025-04-08 09:56:47 +08:00
4b4bf5ed65 ci: Update plugins 2025-04-08 09:53:33 +08:00
a4f78b3a95 build: Update project version 2025-03-24 00:07:35 +08:00
074a58dabd perf: Optimize performance for status view 2025-03-24 00:06:51 +08:00
a2ef175a81 fix: Fix potentially bad formats in culture info 2025-03-24 00:06:29 +08:00
915ba55c2e fix: Correct lifecycle of event listeners 2025-03-24 00:05:35 +08:00
f154a2a468 fix: Fix history list view flashing hard on update 2025-03-24 00:04:29 +08:00
c52d438d40 ci: Update plugins 2025-03-24 00:03:33 +08:00
8d3f53ba13 perf: Improve performance for line renderer 2025-03-20 21:49:28 +08:00
1db25e62e7 fix: Fix wave circle shape when hypocenter is near antimeridian 2025-03-20 21:47:52 +08:00
18312176d9 refactor: Decouple grouping logic from core worker 2025-03-20 17:53:10 +08:00
5be6e32b03 refactor: Pull up map tile cache manager 2025-03-20 17:52:17 +08:00
789 changed files with 17782 additions and 100783 deletions

View File

@@ -2,7 +2,7 @@
"name": "Cryville.Common",
"rootNamespace": "",
"references": [
"GUID:da293eebbcb9a4947a212534c52d1a32"
"GUID:6055be8ebefd69e48b49212b09b47b2f"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@@ -56,7 +56,7 @@ namespace Cryville.Common.Unity.UI {
const float _placeholderLength = 100;
int _firstIndex, _lastIndex;
readonly Stack<GameObject> _pool = new();
void Update() {
void LateUpdate() {
int axis = (int)m_direction;
int sign = m_direction == 0 ? 1 : -1;
float padding = axis == 0 ? m_padding.left : m_padding.top;

View File

@@ -23,6 +23,12 @@ namespace Cryville.Common.Unity.UI {
[SerializeField]
protected bool m_ChildScaleHeight;
[SerializeField]
protected bool m_ChildOverflowWidth;
[SerializeField]
protected bool m_ChildOverflowHeight;
public override void CalculateLayoutInputHorizontal() {
base.CalculateLayoutInputHorizontal();
CalcAlongAxis(0);
@@ -60,9 +66,10 @@ namespace Cryville.Common.Unity.UI {
float size = rectTransform.rect.size[axis];
bool controlSize = (axis == 0) ? m_ChildControlWidth : m_ChildControlHeight;
bool useScale = (axis == 0) ? m_ChildScaleWidth : m_ChildScaleHeight;
bool overflow = (axis == 0) ? m_ChildOverflowWidth : m_ChildOverflowHeight;
bool childForceExpandSize = (axis == 0) ? m_ChildForceExpandWidth : m_ChildForceExpandHeight;
float alignmentOnAxis = GetAlignmentOnAxis(axis);
float innerSize = size - ((axis == 0) ? padding.horizontal : padding.vertical);
float innerSize = overflow ? float.PositiveInfinity : (size - ((axis == 0) ? padding.horizontal : padding.vertical));
RectTransform child = rectChildren[0];
GetChildSizes(child, axis, controlSize, childForceExpandSize, out var min2, out var preferred2, out var flexible2);
float scaleFactor2 = useScale ? child.localScale[axis] : 1f;

View File

@@ -6,8 +6,9 @@ using System.Globalization;
using System.Reflection;
using TMPro;
using UnityEngine;
using UnityEngine.TextCore;
using UnityEngine.TextCore.LowLevel;
using UnityEngine.TextCore.Text;
using AtlasPopulationMode = TMPro.AtlasPopulationMode;
namespace Cryville.Common.Unity.UI {
[RequireComponent(typeof(TMP_Text))]
@@ -16,7 +17,7 @@ namespace Cryville.Common.Unity.UI {
public static FontMatcher FontMatcher;
public static int MaxFallbackCount = 4;
static readonly Dictionary<CultureInfo, FontAsset> _cachedFonts = new();
static readonly Dictionary<CultureInfo, TMP_FontAsset> _cachedFonts = new();
[SerializeField]
Shader m_shader;
@@ -46,7 +47,7 @@ namespace Cryville.Common.Unity.UI {
if (MaxFallbackCount <= 0) break;
}
else {
font.fallbackFontAssetTable ??= new List<FontAsset>();
font.fallbackFontAssetTable ??= new List<TMP_FontAsset>();
font.fallbackFontAssetTable.Add(ifont);
if (font.fallbackFontAssetTable.Count >= MaxFallbackCount) break;
}
@@ -58,23 +59,93 @@ namespace Cryville.Common.Unity.UI {
Text.font = font;
}
static MethodInfo _methodCreateFontAsset;
static readonly object[] _paramsCreateFontAsset = new object[] { null, null, 90, 9, GlyphRenderMode.SDFAA, 1024, 1024, Type.Missing, Type.Missing };
static FontAsset CreateFontAsset(string path, int index) {
if (_methodCreateFontAsset == null) {
_methodCreateFontAsset = typeof(FontAsset).GetMethod(
"CreateFontAsset", BindingFlags.Static | BindingFlags.NonPublic, null,
new Type[] {
typeof(string), typeof(int), typeof(int), typeof(int),
typeof(GlyphRenderMode), typeof(int), typeof(int),
typeof(AtlasPopulationMode), typeof(bool)
},
null
);
static TMP_FontAsset CreateFontAsset(string path, int index) => CreateFontAsset(path, index, 90, 9, GlyphRenderMode.SDFAA, 1024, 1024, AtlasPopulationMode.Dynamic);
static readonly Lazy<FieldInfo> _f_m_Version = new(() => typeof(TMP_FontAsset).GetField("m_Version", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
static readonly Lazy<PropertyInfo> _p_atlasWidth = new(() => typeof(TMP_FontAsset).GetProperty("atlasWidth", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
static readonly Lazy<PropertyInfo> _p_atlasHeight = new(() => typeof(TMP_FontAsset).GetProperty("atlasHeight", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
static readonly Lazy<PropertyInfo> _p_atlasPadding = new(() => typeof(TMP_FontAsset).GetProperty("atlasPadding", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
static readonly Lazy<PropertyInfo> _p_atlasRenderMode = new(() => typeof(TMP_FontAsset).GetProperty("atlasRenderMode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
static readonly Lazy<PropertyInfo> _p_freeGlyphRects = new(() => typeof(TMP_FontAsset).GetProperty("freeGlyphRects", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
static readonly Lazy<PropertyInfo> _p_usedGlyphRects = new(() => typeof(TMP_FontAsset).GetProperty("usedGlyphRects", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
static readonly Lazy<PropertyInfo> _p_ShaderRef_MobileBitmap = new(() => typeof(ShaderUtilities).GetProperty("ShaderRef_MobileBitmap", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic));
static readonly Lazy<PropertyInfo> _p_ShaderRef_MobileSDF = new(() => typeof(ShaderUtilities).GetProperty("ShaderRef_MobileSDF", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic));
public static TMP_FontAsset CreateFontAsset(string path, int faceIndex, int samplingPointSize, int atlasPadding, GlyphRenderMode renderMode, int atlasWidth, int atlasHeight, AtlasPopulationMode atlasPopulationMode = AtlasPopulationMode.Dynamic, bool enableMultiAtlasSupport = true) {
// Initialize FontEngine
FontEngine.InitializeFontEngine();
// Load Font Face
if (FontEngine.LoadFontFace(path, samplingPointSize, faceIndex) != FontEngineError.Success) {
Debug.LogWarning("Unable to load font face at path " + path);
return null;
}
_paramsCreateFontAsset[0] = path;
_paramsCreateFontAsset[1] = index;
return (FontAsset)_methodCreateFontAsset.Invoke(null, _paramsCreateFontAsset);
// Create new font asset
TMP_FontAsset fontAsset = ScriptableObject.CreateInstance<TMP_FontAsset>();
_f_m_Version.Value.SetValue(fontAsset, "1.1.0");
fontAsset.faceInfo = FontEngine.GetFaceInfo();
fontAsset.atlasPopulationMode = atlasPopulationMode;
_p_atlasWidth.Value.SetValue(fontAsset, atlasWidth);
_p_atlasHeight.Value.SetValue(fontAsset, atlasHeight);
_p_atlasPadding.Value.SetValue(fontAsset, atlasPadding);
_p_atlasRenderMode.Value.SetValue(fontAsset, renderMode);
// Initialize array for the font atlas textures.
fontAsset.atlasTextures = new Texture2D[1];
// Create and add font atlas texture.
var texture = new Texture2D(0, 0, TextureFormat.Alpha8, false);
fontAsset.atlasTextures[0] = texture;
fontAsset.isMultiAtlasTexturesEnabled = enableMultiAtlasSupport;
// Add free rectangle of the size of the texture.
int packingModifier;
if (((int)renderMode & 0x10) != 0) {
packingModifier = 0;
// Optimize by adding static ref to shader.
var tmp_material = new Material((Shader)_p_ShaderRef_MobileBitmap.Value.GetValue(null));
//tmp_material.name = texture.name + " Material";
tmp_material.SetTexture(ShaderUtilities.ID_MainTex, texture);
tmp_material.SetFloat(ShaderUtilities.ID_TextureWidth, atlasWidth);
tmp_material.SetFloat(ShaderUtilities.ID_TextureHeight, atlasHeight);
fontAsset.material = tmp_material;
}
else {
packingModifier = 1;
// Optimize by adding static ref to shader.
var tmp_material = new Material((Shader)_p_ShaderRef_MobileSDF.Value.GetValue(null));
//tmp_material.name = texture.name + " Material";
tmp_material.SetTexture(ShaderUtilities.ID_MainTex, texture);
tmp_material.SetFloat(ShaderUtilities.ID_TextureWidth, atlasWidth);
tmp_material.SetFloat(ShaderUtilities.ID_TextureHeight, atlasHeight);
tmp_material.SetFloat(ShaderUtilities.ID_GradientScale, atlasPadding + packingModifier);
tmp_material.SetFloat(ShaderUtilities.ID_WeightNormal, fontAsset.normalStyle);
tmp_material.SetFloat(ShaderUtilities.ID_WeightBold, fontAsset.boldStyle);
fontAsset.material = tmp_material;
}
_p_freeGlyphRects.Value.SetValue(fontAsset, new List<GlyphRect>(8) { new(0, 0, atlasWidth - packingModifier, atlasHeight - packingModifier) });
_p_usedGlyphRects.Value.SetValue(fontAsset, new List<GlyphRect>(8));
// TODO: Consider adding support for extracting glyph positioning data
fontAsset.ReadFontAssetDefinition();
return fontAsset;
}
}
}

View File

@@ -4,7 +4,7 @@ MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
executionOrder: -95
icon: {instanceID: 0}
userData:
assetBundleName:

View File

@@ -1,19 +1,60 @@
using Cryville.Common.Font;
using Cryville.Common.Logging;
using Cryville.Common.Unity.UI;
using Cryville.Culture;
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using UnityEngine;
using Logger = Cryville.Common.Logging.Logger;
namespace Cryville.EEW.Unity {
class App {
public static string AppDataPath { get; private set; }
public static Logger MainLogger { get; private set; }
static FileStream _logFileStream;
static StreamLoggerListener _logWriter;
static bool _init;
public static void Init() {
if (_init) return;
_init = true;
AppDataPath = Application.persistentDataPath;
var logPath = Directory.CreateDirectory(Path.Combine(AppDataPath, "logs"));
_logFileStream = new FileStream(
Path.Combine(
logPath.FullName,
string.Format(
CultureInfo.InvariantCulture,
"{0}.log",
DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmssfff", CultureInfo.InvariantCulture)
)
),
FileMode.Create, FileAccess.Write, FileShare.Read
);
_logWriter = new StreamLoggerListener(_logFileStream) { AutoFlush = true };
MainLogger = new Logger();
var listener = new InstantLoggerListener();
listener.Log += MainLogger.Log;
MainLogger.AddListener(_logWriter);
Application.logMessageReceivedThreaded += OnInternalLog;
MainLogger.Log(1, "App", null, "App Version: {0}", Application.version);
MainLogger.Log(1, "App", null, "Unity Version: {0}", Application.unityVersion);
MainLogger.Log(1, "App", null, "Operating System: {0}, Unity = {1}, Family = {2}", Environment.OSVersion, SystemInfo.operatingSystem, SystemInfo.operatingSystemFamily);
MainLogger.Log(1, "App", null, "Platform: Build = {0}, Unity = {1}", PlatformConfig.Name, Application.platform);
MainLogger.Log(1, "App", null, "Culture: {0}, UI = {1}, Unity = {2}", SharedCultures.CurrentCulture, SharedCultures.CurrentUICulture, Application.systemLanguage);
MainLogger.Log(1, "App", null, "Device: Model = {0}, Type = {1}", SystemInfo.deviceModel, SystemInfo.deviceType);
MainLogger.Log(1, "App", null, "Graphics: Name = {0}, Type = {1}, Vendor = {2}, Version = {3}", SystemInfo.graphicsDeviceName, SystemInfo.graphicsDeviceType, SystemInfo.graphicsDeviceVendor, SystemInfo.graphicsDeviceVersion);
MainLogger.Log(1, "App", null, "Processor: Count = {0}, Frequency = {1}MHz, Type = {2}", SystemInfo.processorCount, SystemInfo.processorFrequency, SystemInfo.processorType);
MainLogger.Log(1, "App", null, "Initializing font manager");
foreach (var res in Resources.LoadAll<TextAsset>("cldr/common/validity")) {
IdValidity.Load(LoadXmlDocument(res));
}
@@ -25,7 +66,10 @@ namespace Cryville.EEW.Unity {
};
TMPLocalizedText.DefaultShader = Resources.Load<Shader>(PlatformConfig.TextShader);
MainLogger.Log(1, "App", null, "Loading config");
SharedSettings.Instance.Init();
MainLogger.Log(1, "App", null, "Initialized");
}
static readonly Encoding _encoding = new UTF8Encoding(false, true);
@@ -40,5 +84,16 @@ namespace Cryville.EEW.Unity {
using var reader = XmlReader.Create(stream, _xmlSettings);
return XDocument.Load(reader);
}
static void OnInternalLog(string condition, string stackTrace, LogType type) {
var l = type switch {
LogType.Log => 1,
LogType.Assert => 2,
LogType.Warning => 3,
LogType.Error or LogType.Exception => 4,
_ => 1,
};
MainLogger.Log(l, "Internal", null, "{0}\n{1}", condition, stackTrace);
}
}
}

View File

@@ -1,3 +1,3 @@
using System.Reflection;
[assembly: AssemblyVersion("0.0.2")]
[assembly: AssemblyVersion("0.0.10")]

View File

@@ -1,5 +1,9 @@
using Cryville.EEW.Core;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Cryville.EEW.Unity {
@@ -8,11 +12,19 @@ namespace Cryville.EEW.Unity {
float SeverityColorMappingLuminanceMultiplier,
bool UseContinuousColor,
string ColorScheme,
float HillshadeLayerOpacity,
string LocationNamer,
string OverrideTimeZone,
bool DoDisplayTimeZone,
bool DoSwitchBackToHistory,
string NowcastWarningDelayTolerance,
string OverrideDisplayCulture,
IReadOnlyCollection<TTSCultureConfig> TTSCultures,
bool DoIgnoreLanguageVariant,
IReadOnlyCollection<EventSourceConfig> EventSources
) {
public static Config Default => new(
@@ -20,11 +32,19 @@ namespace Cryville.EEW.Unity {
1f,
false,
"Default",
1,
"FERegionLong",
null,
true,
true,
"1:00:00",
"",
new List<TTSCultureConfig> { new(SharedCultures.CurrentUICulture) },
true,
new List<EventSourceConfig> {
new JMAAtomEventSourceConfig(Array.Empty<string>()),
new UpdateCheckerEventSourceConfig(),
@@ -37,26 +57,44 @@ namespace Cryville.EEW.Unity {
[JsonDerivedType(typeof(BMKGOpenDataEventSourceConfig), "BMKGOpenData")]
[JsonDerivedType(typeof(CWAOpenDataEventSourceConfig), "CWAOpenData")]
[JsonDerivedType(typeof(EMSCRealTimeEventSourceConfig), "EMSCRealTime")]
[JsonDerivedType(typeof(FANStudioEventSourceConfig), "FANStudio")]
[JsonDerivedType(typeof(GeoNetEventSourceConfig), "GeoNet")]
[JsonDerivedType(typeof(GlobalQuakeServerEventSourceConfig), "GlobalQuakeServer")]
[JsonDerivedType(typeof(GlobalQuakeServer15EventSourceConfig), "GlobalQuakeServer15")]
[JsonDerivedType(typeof(JMAAtomEventSourceConfig), "JMAAtom")]
[JsonDerivedType(typeof(NOAAEventSourceConfig), "NOAA")]
[JsonDerivedType(typeof(UpdateCheckerEventSourceConfig), "UpdateChecker")]
[JsonDerivedType(typeof(USGSQuakeMLEventSourceConfig), "USGSQuakeML")]
[JsonDerivedType(typeof(USGSEventSourceConfig), "USGSQuakeML")]
[JsonDerivedType(typeof(WolfxEventSourceConfig), "Wolfx")]
abstract record EventSourceConfig();
record BMKGOpenDataEventSourceConfig([property: JsonRequired] string[] Subtypes) : EventSourceConfig;
record CWAOpenDataEventSourceConfig([property: JsonRequired] string Subtype, [property: JsonRequired] string Token) : EventSourceConfig;
record EMSCRealTimeEventSourceConfig() : EventSourceConfig;
record FANStudioEventSourceConfig([property: JsonRequired] string Subtype) : EventSourceConfig;
record GeoNetEventSourceConfig(int MinimumMMI = 3, bool DoGetFullHistory = false, bool DoGetStrongMotionInfo = true) : 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 USGSQuakeMLEventSourceConfig([property: JsonRequired] string Subtype) : EventSourceConfig;
record WolfxEventSourceConfig(IReadOnlyCollection<string> Filter = null, bool IsFilterWhitelist = false) : EventSourceConfig;
record USGSEventSourceConfig([property: JsonRequired] string Subtype, bool UseGeoJSONFeeds, string[] Products) : EventSourceConfig;
record WolfxEventSourceConfig(IReadOnlyCollection<string> Filter = null, bool IsFilterWhitelist = false, bool UseRawCENCLocationName = false) : EventSourceConfig;
[JsonSerializable(typeof(Config))]
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSourceGenerationOptions(Converters = new Type[] { typeof(CultureInfoConverter) }, WriteIndented = true)]
sealed partial class ConfigSerializationContext : JsonSerializerContext { }
sealed class CultureInfoConverter : JsonConverter<CultureInfo> {
public override CultureInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
Debug.Assert(typeToConvert == typeof(CultureInfo));
var value = reader.GetString();
if (value == null) return CultureInfo.InvariantCulture;
if (value == "") return SharedCultures.CurrentUICulture;
return SharedCultures.Get(value);
}
public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options) {
writer.WriteStringValue(value.Name);
}
}
}

View File

@@ -3,8 +3,9 @@
"rootNamespace": "",
"references": [
"GUID:b92f9c7ac10b1c04e86fc48210f62ab1",
"GUID:1e0937e40dadba24a97b7342c4559580",
"GUID:e5b7e7f40a80a814ba706299d68f9213",
"GUID:da293eebbcb9a4947a212534c52d1a32"
"GUID:6055be8ebefd69e48b49212b09b47b2f"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@@ -1,3 +1,4 @@
using Cryville.EEW.Core.Map;
using System;
using System.Drawing;
using UnityEngine;
@@ -10,26 +11,51 @@ namespace Cryville.EEW.Unity.Map {
[SerializeField]
Transform m_layerTile;
[SerializeField]
Transform m_layerTileHillshade;
[SerializeField]
MapElementManager m_layerElement;
[SerializeField]
MapElementManager m_layerElementSub;
[SerializeField]
GameObject m_prefabTile;
[SerializeField]
GameObject m_prefabTileHillshade;
[SerializeField]
GameObject m_prefabBitmapHolder;
[SerializeField]
int m_maxMapTileZoom = 10;
[SerializeField]
bool m_isEditor;
readonly MapTileCacheManager _tiles = new();
MapTileCacheManager _tiles;
MapTileCacheManager _tilesHillshade;
float _elementLayerZ;
void Start() {
_camera = GetComponent<Camera>();
_tiles = m_isEditor ? new EditorMapTileCacheManager() : new MapTileCacheManager();
_tiles.ExtraCachedZoomLevel = 20;
_tiles.Parent = m_layerTile;
_tiles.PrefabTile = m_prefabTile;
_tiles.PrefabBitmapHolder = m_prefabBitmapHolder;
_tiles.CacheDir = Application.temporaryCachePath;
if (m_layerTileHillshade) {
m_prefabTileHillshade.GetComponent<SpriteRenderer>().sharedMaterial.color = new UnityEngine.Color(1, 1, 1, SharedSettings.Instance.HillshadeLayerOpacity);
_tilesHillshade = new HillshadeMapTileCacheManager {
ExtraCachedZoomLevel = 20,
Parent = m_layerTileHillshade,
PrefabTile = m_prefabTileHillshade,
PrefabBitmapHolder = m_prefabBitmapHolder,
CacheDir = Application.temporaryCachePath,
};
}
_camera.orthographicSize = 0.5f / MathF.Max(1, (float)_camera.pixelWidth / _camera.pixelHeight);
_elementLayerZ = m_layerElement.transform.position.z;
if (m_layerElement != null) _elementLayerZ = m_layerElement.transform.position.z;
_mapElementUpdated = true;
}
void OnDestroy() {
_tiles.Dispose();
_tilesHillshade?.Dispose();
}
float Scale {
@@ -70,7 +96,7 @@ namespace Cryville.EEW.Unity.Map {
}
}
void ZoomToMapElement() {
var aabb = m_layerElement.AABB;
var aabb = m_layerElement != null ? m_layerElement.AABB : null;
if (aabb is not RectangleF b) return;
if (b.Width * _camera.pixelHeight < _camera.pixelWidth * b.Height)
Scale = b.Height;
@@ -93,26 +119,30 @@ namespace Cryville.EEW.Unity.Map {
transform.localPosition = new(nx, Math.Clamp(transform.position.y, h / 2 - 1, -h / 2), -20);
var bounds = new Bounds((Vector2)transform.position, new Vector2(w, h));
int zoom = Math.Clamp((int)Math.Log(vz / 256, 2) + 1, 0, 10);
int zoom = Math.Clamp((int)Math.Log(vz / 256, 2) + 1, 0, m_maxMapTileZoom);
int zoomScale = 1 << zoom;
_tiles.MoveTo(
new(Mathf.FloorToInt(bounds.min.x * zoomScale), Mathf.FloorToInt(-bounds.max.y * zoomScale), zoom),
new(Mathf.CeilToInt(bounds.max.x * zoomScale), Mathf.CeilToInt(-bounds.min.y * zoomScale), zoom)
);
var a = new MapTileIndex(Mathf.FloorToInt(bounds.min.x * zoomScale), Mathf.FloorToInt(-bounds.max.y * zoomScale), zoom);
var b = new MapTileIndex(Mathf.CeilToInt(bounds.max.x * zoomScale), Mathf.CeilToInt(-bounds.min.y * zoomScale), zoom);
_tiles.MoveTo(a, b);
_tilesHillshade?.MoveTo(a, b);
m_layerElement.Scale = h;
m_layerElementSub.Scale = h;
if (m_layerElement != null) {
m_layerElement.Scale = h;
}
if (m_layerElementSub != null) {
m_layerElementSub.Scale = h;
if (nx - w / 2 < 0) {
m_layerElementSub.gameObject.SetActive(true);
m_layerElementSub.transform.localPosition = new(-1, 0, _elementLayerZ);
}
else if (nx + w / 2 > 1) {
m_layerElementSub.gameObject.SetActive(true);
m_layerElementSub.transform.localPosition = new(1, 0, _elementLayerZ);
}
else {
m_layerElementSub.gameObject.SetActive(false);
if (nx - w / 2 < 0) {
m_layerElementSub.gameObject.SetActive(true);
m_layerElementSub.transform.localPosition = new(-1, 0, _elementLayerZ);
}
else if (nx + w / 2 > 1) {
m_layerElementSub.gameObject.SetActive(true);
m_layerElementSub.transform.localPosition = new(1, 0, _elementLayerZ);
}
else {
m_layerElementSub.gameObject.SetActive(false);
}
}
}
}

View File

@@ -0,0 +1,15 @@
using Cryville.EEW.Core.Map;
using System.IO;
using UnityEngine;
namespace Cryville.EEW.Unity.Map {
sealed class EditorMapTileCacheManager : MapTileCacheManager {
protected override MapTileBitmapHolder CreateBitmapHolder(MapTileIndex index) => new(
index,
GameObject.Instantiate(PrefabBitmapHolder, Parent, false),
new($"https://tile.openstreetmap.org/{index.Z}/{index.NX}/{index.NY}.png")
);
protected override string GetCacheFilePath(MapTileIndex index) => Path.Combine(CacheDir, $"map_editor/{index.Z}/{index.NX}/{index.NY}");
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 9e8aa96a139a7414cb4d2031a22588db
guid: 478198b8ecc0082449fa3f68795174a9
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -98,24 +98,25 @@ namespace Cryville.EEW.Unity.Map.Element {
for (int d = 0; d < 360; d++) {
Quaternion q = Quaternion.AngleAxis(d, axis);
Vector3 p = q * rp;
Vector2 p2 = ToTilePos(p).ToVector2();
if (lp2 != null) {
float dx = p2.x - lp2.Value.x;
if (MathF.Abs(dx) >= 0.5) {
_vertexBuffer[d] = p2.x < 0.5 ? p2 + new Vector2(1, 0) : p2 - new Vector2(1, 0);
renderer.AddSegment(_vertexBuffer, segmentIndex, d - segmentIndex + 1);
segmentIndex = d;
}
}
_vertexBuffer[d] = p2;
lp2 = p2;
AddVertex(renderer, ref lp2, ref segmentIndex, d, p);
}
Vector2 rp2 = ToTilePos(rp).ToVector2();
_vertexBuffer[360] = rp2;
AddVertex(renderer, ref lp2, ref segmentIndex, 360, rp);
renderer.AddSegment(_vertexBuffer, segmentIndex, 361 - segmentIndex);
}
renderer.SetMaterial(isHistory ? m_historyMaterial : m_ongoingMaterial);
}
void AddVertex(MultiLineRenderer renderer, ref Vector2? lp2, ref int segmentIndex, int d, Vector3 p) {
Vector2 p2 = ToTilePos(p).ToVector2();
if (lp2 != null && MathF.Abs(p2.x - lp2.Value.x) >= 0.5) {
_vertexBuffer[d] = p2.x < 0.5 ? p2 + new Vector2(1, 0) : p2 - new Vector2(1, 0);
renderer.AddSegment(_vertexBuffer, segmentIndex, d - segmentIndex + 1);
segmentIndex = d;
}
_vertexBuffer[d] = p2;
lp2 = p2;
}
static PointF ToTilePos(Vector3 p) => MapTileUtils.WorldToTilePos(new(MathF.Atan2(p.z, p.x) / MathF.PI * 180f, MathF.Asin(p.y) / MathF.PI * 180f));
}
}

View File

@@ -0,0 +1,15 @@
using Cryville.EEW.Core.Map;
using System.IO;
using UnityEngine;
namespace Cryville.EEW.Unity.Map {
sealed class HillshadeMapTileCacheManager : MapTileCacheManager {
protected override MapTileBitmapHolder CreateBitmapHolder(MapTileIndex index) => new(
index,
GameObject.Instantiate(PrefabBitmapHolder, Parent, false),
new($"https://services.arcgisonline.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer/tile/{index.Z}/{index.NY}/{index.NX}.png")
);
protected override string GetCacheFilePath(MapTileIndex index) => Path.Combine(CacheDir, $"map_hillshade/{index.Z}/{index.NX}/{index.NY}");
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 4f85302336d038c4da80ea264d185657
guid: d5fed9b884a4ff54f837aa0f2265ad36
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -150,12 +150,8 @@ namespace Cryville.EEW.Unity.Map {
_mesh.Clear();
if (_positions == null) return;
if (_positionCount <= 1) return;
float hw = m_width / 2;
int maxVertexCount = 4 * (_positionCount - 1);
var vbuf = ArrayPool<Vector3>.Shared.Rent(maxVertexCount);
var ubuf = ArrayPool<Vector2>.Shared.Rent(maxVertexCount);
var ibuf = ArrayPool<int>.Shared.Rent(3 * (2 + 4 * (_positionCount - 2)));
float hw = m_width / 2;
int i, vi = 0, ii = 0, li = 0, ri = 1;
float uvScale = 1 / (m_tilingScale * m_width);
Vector2 p0 = _positions[0], p1 = default;
@@ -163,6 +159,12 @@ namespace Cryville.EEW.Unity.Map {
if ((p1 = _positions[i]) != p0) break;
}
if (i >= _positionCount) return;
int maxVertexCount = 4 * (_positionCount - 1);
var vbuf = ArrayPool<Vector3>.Shared.Rent(maxVertexCount);
var ubuf = ArrayPool<Vector2>.Shared.Rent(maxVertexCount);
var ibuf = ArrayPool<int>.Shared.Rent(3 * (2 + 4 * (_positionCount - 2)));
Vector2 dp0 = NormalizeSmallVector(p1 - p0), np0 = GetNormal(dp0 * hw);
vbuf[vi] = p0 - np0; ubuf[vi++] = new(0, 0);
vbuf[vi] = p0 + np0; ubuf[vi++] = new(0, 1);

View File

@@ -2,12 +2,15 @@ using Cryville.EEW.BMKGOpenData.Map;
using Cryville.EEW.Core;
using Cryville.EEW.CWAOpenData.Map;
using Cryville.EEW.EMSC.Map;
using Cryville.EEW.FANStudio.Map;
using Cryville.EEW.GeoNet.Map;
using Cryville.EEW.GlobalQuake.Map;
using Cryville.EEW.JMAAtom.Map;
using Cryville.EEW.Map;
using Cryville.EEW.NOAA.Map;
using Cryville.EEW.QuakeML.Map;
using Cryville.EEW.Report;
using Cryville.EEW.USGS.Map;
using Cryville.EEW.Wolfx.Map;
using System.Collections.Generic;
using System.Drawing;
@@ -128,19 +131,27 @@ namespace Cryville.EEW.Unity.Map {
readonly ContextedGeneratorManager<IMapGeneratorContext, MapElement> _gen = new(new IContextedGenerator<IMapGeneratorContext, MapElement>[] {
new BMKGEarthquakeMapGenerator(),
new CEAEEWMapGenerator(),
new CENCEarthquakeMapGenerator(),
new CENCEEWMapGenerator(),
new CWAEarthquakeMapGenerator(),
new CWAEEWMapGenerator(),
new CWATsunamiMapGenerator(),
new EMSCRealTimeEventMapGenerator(),
new FujianEEWMapGenerator(),
new FANStudio.Map.FujianEEWMapGenerator(),
new Wolfx.Map.FujianEEWMapGenerator(),
new GeoNetQuakeHistoryMapGenerator(),
new GeoNetQuakeMapGenerator(),
new GeoNetStrongMapGenerator(),
new GlobalQuakeMapViewGenerator(),
new ICLEEWMapGenerator(),
new JMAAtomMapGenerator(),
new JMAEEWMapGenerator(),
new NOAAMapGenerator(),
new FANStudio.Map.SichuanEEWMapGenerator(),
new Wolfx.Map.SichuanEEWMapGenerator(),
new QuakeMLEventMapGenerator(),
new SichuanEEWMapGenerator(),
new USGSContoursMapGenerator(),
});
public UnityMapElement Build(object e, out CultureInfo culture, out int order) {
culture = CultureInfo.InvariantCulture;

View File

@@ -1,113 +1,50 @@
using Cryville.EEW.Core.Map;
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
namespace Cryville.EEW.Unity.Map {
[RequireComponent(typeof(SpriteRenderer))]
sealed class MapTile : MonoBehaviour {
static readonly SemaphoreSlim _semaphore = new(2);
static readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(10) };
[SerializeField] Transform _idView;
public MapTileIndex Index { get; set; }
public bool IsEmpty { get; private set; }
Action<MapTile> _callback;
SpriteRenderer _renderer;
UnityWebRequest _req;
DownloadHandlerTexture _texHandler;
Texture2D _tex;
Sprite _sprite;
void Awake() {
_renderer = GetComponent<SpriteRenderer>();
}
FileInfo _localFile;
bool _downloadDone;
public void Load(MapTileIndex index, string cacheDir, Action<MapTile> onUpdated) {
public void Init(MapTileIndex index) {
Index = index;
_callback = onUpdated;
_localFile = new(Path.Combine(cacheDir, $"map/{Index.Z}/{Index.NX}/{Index.NY}"));
float z = 1 << index.Z;
transform.localPosition = new(index.X / z, -(index.Y + 1) / z, -index.Z / 100f);
transform.localScale = new Vector3(1 / z, 1 / z, 1);
_idView.gameObject.SetActive(true);
byte e = SharedSettings.Instance.IdBytes[((index.X << 2) + index.Y) & 0x1f];
int ex = e >> 4, ey = e & 0xf;
_idView.localPosition = new(ex / 16f, 1 - ey / 16f, -1 / 200f);
if (_localFile.Exists) {
_downloadDone = true;
if (_idView) {
_idView.gameObject.SetActive(true);
byte e = SharedSettings.Instance.IdBytes[((index.X << 2) + index.Y) & 0x1f];
int ex = e >> 4, ey = e & 0xf;
_idView.localPosition = new(ex / 16f, 1 - ey / 16f, -1 / 200f);
}
else {
Task.Run(() => RunAsync($"https://server.arcgisonline.com/ArcGIS/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{Index.Z}/{Index.NY}/{Index.NX}"));
}
}
async Task RunAsync(string url) {
await _semaphore.WaitAsync().ConfigureAwait(true);
try {
Directory.CreateDirectory(_localFile.DirectoryName);
using var webStream = await _httpClient.GetStreamAsync(new Uri(url)).ConfigureAwait(true);
using var fileStream = new FileStream(_localFile.FullName, FileMode.Create, FileAccess.Write);
await webStream.CopyToAsync(fileStream).ConfigureAwait(true);
}
finally {
_semaphore.Release();
}
_downloadDone = true;
}
public void Set(Sprite sprite) {
if (_renderer) {
_renderer.sprite = sprite;
}
}
bool _isDestroyed;
public void Destroy() {
_isDestroyed = true;
}
void Update() {
if (_downloadDone) {
try {
_texHandler = new DownloadHandlerTexture();
_req = new UnityWebRequest($"file:///{_localFile}") {
downloadHandler = _texHandler,
disposeDownloadHandlerOnDispose = true,
};
_req.SendWebRequest();
}
catch (Exception ex) {
Debug.LogException(ex);
}
_downloadDone = false;
if (_isDestroyed) {
Destroy(gameObject);
}
if (_req == null || !_req.isDone) return;
if (_texHandler.isDone) {
_tex = _texHandler.texture;
_tex.wrapMode = TextureWrapMode.Clamp;
_sprite = Sprite.Create(_tex, new Rect(0, 0, _tex.width, _tex.height), Vector2.zero, _tex.height, 0, SpriteMeshType.FullRect, Vector4.zero, false);
_renderer.sprite = _sprite;
}
else {
Debug.LogError(_req.error);
_localFile.Delete();
IsEmpty = true;
}
_req.Dispose();
_req = null;
_callback?.Invoke(this);
}
void OnDestroy() {
if (_req != null) {
_req.Abort();
_req.Dispose();
_texHandler.Dispose();
}
if (_sprite) Destroy(_sprite);
if (_tex) Destroy(_tex);
IsEmpty = true;
_callback?.Invoke(this);
public void SetVisible(bool v) {
_renderer.enabled = v;
}
}
}

View File

@@ -0,0 +1,49 @@
using Cryville.EEW.Core.Map;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
namespace Cryville.EEW.Unity.Map {
sealed class MapTileBitmapHolder : Core.Map.MapTileBitmapHolder {
readonly MapTileBitmapHolderBehaviour _behaviour;
readonly Uri _uri;
public MapTileBitmapHolder(MapTileIndex index, GameObject gameObject, Uri uri) : base(index) {
_behaviour = gameObject.GetComponent<MapTileBitmapHolderBehaviour>();
_behaviour.Index = index;
_uri = uri;
}
protected override void Dispose(bool disposing) {
base.Dispose(disposing);
if (disposing) {
if (_behaviour) _behaviour.Destroy();
}
}
protected override Uri GetUri() => _uri;
protected override Task LoadBitmap(FileInfo file, CancellationToken cancellationToken) {
_behaviour.Load(file);
return Task.CompletedTask;
}
public void Bind(MapTile tile) {
_behaviour.Bind(tile);
}
public void Unbind(MapTile tile) {
_behaviour.Unbind(tile);
}
public void AddChild(MapTileBitmapHolder bitmapHolder) {
_behaviour.AddChild(bitmapHolder._behaviour);
}
public void RemoveChild(MapTileBitmapHolder bitmapHolder) {
_behaviour.RemoveChild(bitmapHolder._behaviour);
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 225b67dcce9247b4c806e435b34695d2
guid: f7ad3a3ac7d829249ba21987d585b07f
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,127 @@
using Cryville.EEW.Core.Map;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
namespace Cryville.EEW.Unity.Map {
sealed class MapTileBitmapHolderBehaviour : MonoBehaviour {
public MapTileIndex Index { get; set; }
readonly List<MapTile> _tiles = new();
public void Bind(MapTile tile) {
_tiles.Add(tile);
if (_sprite) {
tile.Set(_sprite);
}
}
public void Unbind(MapTile tile) {
_tiles.Remove(tile);
}
readonly List<MapTileBitmapHolderBehaviour> _children = new();
public void AddChild(MapTileBitmapHolderBehaviour behaviour) {
_children.Add(behaviour);
foreach (var tile in _tiles) {
tile.SetVisible(false);
}
SetChildSprite(behaviour);
}
public void RemoveChild(MapTileBitmapHolderBehaviour behaviour) {
_children.Remove(behaviour);
bool isActive = _children.Count == 0;
foreach (var tile in _tiles) {
tile.SetVisible(isActive);
}
}
UnityWebRequest _req;
DownloadHandlerTexture _texHandler;
Texture2D _tex;
Sprite _sprite;
FileInfo _localFile;
bool _isReady;
public void Load(FileInfo file) {
_localFile = file;
_isReady = true;
}
void Update() {
if (_isDestroyed) {
Destroy(gameObject);
return;
}
if (_isReady) {
try {
_texHandler = new DownloadHandlerTexture();
_req = new UnityWebRequest($"file:///{_localFile}") {
downloadHandler = _texHandler,
disposeDownloadHandlerOnDispose = true,
};
_req.SendWebRequest();
}
catch (Exception ex) {
App.MainLogger.Log(4, "Map", null, "An error occurred when loading map tile {0}: {1}", _localFile, ex);
}
_isReady = false;
}
if (_req == null || !_req.isDone) return;
if (_texHandler.isDone && _texHandler.texture != null) {
_tex = _texHandler.texture;
_tex.wrapMode = TextureWrapMode.Clamp;
SetSprite(_tex, 0, 0, 0);
}
else {
App.MainLogger.Log(4, "Map", null, "An error occurred when loading map tile {0}: {1}", _localFile, _texHandler.error);
_localFile.Delete();
}
_req.Dispose();
_texHandler.Dispose();
_req = null;
}
int _minDz = int.MaxValue;
int _x, _y;
void SetSprite(Texture2D tex, int dz, int x, int y) {
if (dz >= 8) return;
if (dz > _minDz) return;
_tex = tex;
_x = x; _y = y;
_minDz = dz;
if (_sprite) Destroy(_sprite);
int sx = x << (8 - dz), sy = 256 - ((y + 1) << (8 - dz));
_sprite = Sprite.Create(tex, new Rect(sx, sy, 1 << (8 - dz), 1 << (8 - dz)), Vector2.zero, 1 << (8 - dz), 0, SpriteMeshType.FullRect, Vector4.zero, false);
foreach (var tile in _tiles)
tile.Set(_sprite);
foreach (var child in _children) {
SetChildSprite(child);
}
}
void SetChildSprite(MapTileBitmapHolderBehaviour child) {
if (!_tex) return;
int cdz = child.Index.Z - Index.Z;
int cx = child.Index.X % (1 << cdz) | (_x << cdz), cy = child.Index.Y % (1 << cdz) | (_y << cdz);
child.SetSprite(_tex, cdz + _minDz, cx, cy);
}
bool _isDestroyed;
public void Destroy() {
_isDestroyed = true;
}
void OnDestroy() {
if (_req != null) {
_req.Abort();
if (_texHandler != null) {
var tex = _texHandler.texture;
if (tex) Destroy(tex);
_texHandler.Dispose();
}
_req.Dispose();
}
if (_sprite) Destroy(_sprite);
if (_tex && _minDz == 0) Destroy(_tex);
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: f0dddddc28f728c42a1fed80ba8f95cc
guid: c17f6b26e9a6bd74e8b2d071c6951c41
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -1,166 +1,56 @@
using Cryville.EEW.Core.Map;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace Cryville.EEW.Unity.Map {
sealed class TileZOrderComparer : IComparer<MapTileIndex>, IComparer<MapTile> {
public static readonly TileZOrderComparer Instance = new();
public int Compare(MapTileIndex a, MapTileIndex b) {
var c = a.Z.CompareTo(b.Z);
if (c != 0) return c;
c = a.Y.CompareTo(b.Y);
if (c != 0) return c;
return a.X.CompareTo(b.X);
}
public int Compare(MapTile a, MapTile b) {
if (a == null) return b == null ? 0 : -1;
if (b == null) return 1;
return Compare(a.Index, b.Index);
}
}
sealed class MapTileCacheManager : IDisposable {
public int ExtraCachedZoomLevel { get; set; } = 20;
GameObject m_prefabTile;
public GameObject PrefabTile {
get => m_prefabTile;
set {
m_prefabTile = value;
if (_dummyTask) GameObject.Destroy(_dummyTask.gameObject);
_dummyTask = GameObject.Instantiate(m_prefabTile, Parent, false).GetComponent<MapTile>();
}
}
class MapTileCacheManager : MapTileCacheManager<MapTileBitmapHolder> {
public GameObject PrefabTile { get; set; }
public GameObject PrefabBitmapHolder { get; set; }
public Transform Parent { get; set; }
public string CacheDir { get; set; }
public event Action Updated;
void OnUpdated(MapTile tile) {
if (tile.IsEmpty) {
lock (ActiveTiles) {
if (_cache.Remove(tile.Index)) {
ActiveTiles.RemoveAt(ActiveTiles.BinarySearch(tile, TileZOrderComparer.Instance));
}
}
}
Updated?.Invoke();
protected override MapTileBitmapHolder CreateBitmapHolder(MapTileIndex index) => new(
index,
GameObject.Instantiate(PrefabBitmapHolder, Parent, false),
new($"https://services.arcgisonline.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{index.Z}/{index.NY}/{index.NX}")
);
protected override string GetCacheFilePath(MapTileIndex index) => Path.Combine(CacheDir, $"map/{index.Z}/{index.NX}/{index.NY}");
readonly Dictionary<MapTile<MapTileBitmapHolder>, MapTile> _map = new();
protected override void OnTileCreated(MapTile<MapTileBitmapHolder> tile) {
base.OnTileCreated(tile);
var gameObject = GameObject.Instantiate(PrefabTile, Parent, false);
var uTile = gameObject.GetComponent<MapTile>();
_map.Add(tile, uTile);
var index = tile.Index;
uTile.Init(index);
var bitmapHolder = tile.BitmapHolder;
bitmapHolder.Bind(uTile);
if (index.Z <= 0) return;
var maybeTile = FindTile(index.ZoomToLevel(index.Z - 1, Math.Floor), false);
if (maybeTile is not MapTile<MapTileBitmapHolder> parentTile) return;
parentTile.BitmapHolder.AddChild(bitmapHolder);
}
public void Dispose() {
MonoBehaviour.Destroy(_dummyTask);
lock (ActiveTiles) {
foreach (var task in ActiveTiles)
GameObject.Destroy(task.gameObject);
ActiveTiles.Clear();
_cache.Clear();
}
}
readonly Dictionary<MapTileIndex, MapTile> _cache = new();
public List<MapTile> ActiveTiles { get; } = new();
MapTileIndex _a, _b;
public void MoveTo(MapTileIndex a, MapTileIndex b) {
if (a.Z != b.Z) throw new ArgumentException("Mismatched Z index.");
if (a.X >= b.X || a.Y >= b.Y) throw new ArgumentException("Incorrect relative X/Y index.");
lock (ActiveTiles) {
UnloadTiles(a, b);
_a = a; _b = b;
LoadTiles(a, b);
}
}
void LoadTiles(MapTileIndex a, MapTileIndex b) {
for (int z = Math.Max(0, a.Z - ExtraCachedZoomLevel); z <= Math.Max(0, a.Z); z++) {
var ia = a.ZoomToLevel(z, Math.Floor);
var ib = b.ZoomToLevel(z, Math.Ceiling);
for (int x = ia.X; x < ib.X; x++) {
for (int y = ia.Y; y < ib.Y; y++) {
var index = new MapTileIndex(x, y, z);
if (_cache.ContainsKey(index)) continue;
var task = GameObject.Instantiate(PrefabTile, Parent, false).GetComponent<MapTile>();
task.Load(index, CacheDir, OnUpdated);
_cache.Add(index, task);
var i = ~ActiveTiles.BinarySearch(task, TileZOrderComparer.Instance);
ActiveTiles.Insert(i, task);
}
}
}
}
void UnloadTiles(MapTileIndex a, MapTileIndex b) {
if (a.Z != _a.Z) {
for (int z = _a.Z - ExtraCachedZoomLevel; z < a.Z - ExtraCachedZoomLevel; z++) UnloadTilesAtZoomLevel(z);
for (int z = a.Z + 1; z <= _a.Z; z++) UnloadTilesAtZoomLevel(z);
}
if (a.X > _a.X) {
for (int z = Math.Max(0, a.Z); z >= Math.Max(0, a.Z - ExtraCachedZoomLevel); --z) {
var ia0 = _a.ZoomToLevel(z, Math.Floor);
var ib0 = _b.ZoomToLevel(z, Math.Ceiling);
var ia1 = a.ZoomToLevel(z, Math.Floor);
if (ia0.X == ia1.X) break;
UnloadTilesInRegion(ia0.X, ia0.Y, ia1.X, ib0.Y, z);
}
}
if (b.X < _b.X) {
for (int z = Math.Max(0, a.Z); z >= Math.Max(0, a.Z - ExtraCachedZoomLevel); --z) {
var ia0 = _a.ZoomToLevel(z, Math.Floor);
var ib0 = _b.ZoomToLevel(z, Math.Ceiling);
var ib1 = b.ZoomToLevel(z, Math.Ceiling);
if (ib0.X == ib1.X) break;
UnloadTilesInRegion(ib1.X, ia0.Y, ib0.X, ib0.Y, z);
}
}
if (a.Y > _a.Y) {
for (int z = Math.Max(0, a.Z); z >= Math.Max(0, a.Z - ExtraCachedZoomLevel); --z) {
var ia0 = _a.ZoomToLevel(z, Math.Floor);
var ib0 = _b.ZoomToLevel(z, Math.Ceiling);
var ia1 = a.ZoomToLevel(z, Math.Floor);
if (ia0.Y == ia1.Y) break;
UnloadTilesInRegion(ia0.X, ia0.Y, ib0.X, ia1.Y, z);
}
}
if (b.Y < _b.Y) {
for (int z = Math.Max(0, a.Z); z >= Math.Max(0, a.Z - ExtraCachedZoomLevel); --z) {
var ia0 = _a.ZoomToLevel(z, Math.Floor);
var ib0 = _b.ZoomToLevel(z, Math.Ceiling);
var ib1 = b.ZoomToLevel(z, Math.Ceiling);
if (ib0.Y == ib1.Y) break;
UnloadTilesInRegion(ia0.X, ib1.Y, ib0.X, ib0.Y, z);
}
}
}
void UnloadTilesInRegion(int x1, int y1, int x2, int y2, int z) {
for (int x = x1; x < x2; x++) {
for (int y = y1; y < y2; y++) {
var index = new MapTileIndex(x, y, z);
if (!_cache.TryGetValue(index, out var task)) continue;
GameObject.Destroy(task.gameObject);
}
}
}
MapTile _dummyTask;
void UnloadTilesAtZoomLevel(int z) {
if (z < 0) return;
_dummyTask.Index = new(int.MinValue, int.MinValue, z);
var i0 = ActiveTiles.BinarySearch(_dummyTask, TileZOrderComparer.Instance);
if (i0 < 0) i0 = ~i0;
_dummyTask.Index = new(int.MinValue, int.MinValue, z + 1);
var i1 = ActiveTiles.BinarySearch(_dummyTask, TileZOrderComparer.Instance);
if (i1 < 0) i1 = ~i1;
for (var i = i1 - 1; i >= i0; --i) {
var index = ActiveTiles[i].Index;
if (!_cache.TryGetValue(index, out var task)) continue;
GameObject.Destroy(task.gameObject);
protected override void OnTileDestroyed(MapTile<MapTileBitmapHolder> tile) {
base.OnTileDestroyed(tile);
var bitmapHolder = tile.BitmapHolder;
if (_map.TryGetValue(tile, out var uTile)) {
uTile.Destroy();
bitmapHolder.Unbind(uTile);
_map.Remove(tile);
}
var index = tile.Index;
if (index.Z <= 0) return;
var maybeTile = FindTile(index.ZoomToLevel(index.Z - 1, Math.Floor), false);
if (maybeTile is not MapTile<MapTileBitmapHolder> parentTile) return;
parentTile.BitmapHolder.RemoveChild(bitmapHolder);
}
}
}

View File

@@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using TMPro;
using UnityEngine;
namespace Cryville.EEW.Unity.Map {
sealed class RegionEditor : MonoBehaviour {
QuadTreeNode _root;
[SerializeField] CameraController m_cameraController;
[SerializeField] GameObject m_regionViewPrefab;
[SerializeField] TMP_Text m_textSelectedInfo;
[SerializeField] TMP_Text m_textHoveredInfo;
[SerializeField] TMP_InputField m_inputId;
readonly Dictionary<QuadTreeNode, RegionView> _map = new();
void Start() {
var file = new FileInfo(Path.Combine(Application.persistentDataPath, "regions.json"));
if (file.Exists) {
using var stream = file.OpenRead();
_root = JsonSerializer.Deserialize<QuadTreeNode>(stream);
}
else {
_root = NewNode();
}
BuildView(_root);
}
public void Save() {
var file = new FileInfo(Path.Combine(Application.persistentDataPath, "regions.json"));
using var stream = file.Open(FileMode.Create);
JsonSerializer.Serialize(stream, _root);
}
void BuildView(QuadTreeNode node) {
var view = Instantiate(m_regionViewPrefab, transform, false).GetComponent<RegionView>();
view.Init(node.X, node.Y, node.Z);
view.Id = node.Data.Id;
view.IsLeaf = node.Children == null;
_map.Add(node, view);
BuildChildViews(node);
}
void BuildChildViews(QuadTreeNode node) {
if (node.Children == null) return;
foreach (var child in node.Children) {
BuildView(child);
}
}
void DestroyChildViews(QuadTreeNode node) {
if (node.Children == null) return;
foreach (var child in node.Children) {
Destroy(_map[child].gameObject);
_map.Remove(child);
}
}
QuadTreeNode _hoveredNode;
QuadTreeNode _selectedNode;
Vector3? _ppos;
void Update() {
var pos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
pos.y += 1;
var hoveredNode = _root.Get(pos);
if (hoveredNode != _hoveredNode) {
HoverNode(hoveredNode);
}
if (Input.GetMouseButtonDown(0)) {
_ppos = Input.mousePosition;
}
if (Input.GetMouseButton(0) && _ppos is Vector3 pos0) {
if (Input.mousePosition != pos0) {
_ppos = null;
}
}
if (hoveredNode == null) return;
if (Input.GetMouseButtonUp(0) && _ppos != null) {
SelectNode(hoveredNode);
_ppos = null;
}
if (m_inputId.isFocused)
return;
if (Input.GetKeyUp(KeyCode.A)) {
MergeNode(hoveredNode);
}
if (Input.GetKeyUp(KeyCode.S)) {
SplitNode(hoveredNode);
}
if (Input.GetKeyUp(KeyCode.C)) {
m_inputId.text = hoveredNode.Data.Id;
}
if (Input.GetKeyUp(KeyCode.V)) {
hoveredNode.Data.Id = m_inputId.text;
_map[hoveredNode].Id = hoveredNode.Data.Id;
}
}
void HoverNode(QuadTreeNode node) {
if (_hoveredNode != null) {
_map[_hoveredNode].IsHovered = false;
}
_hoveredNode = node;
if (_hoveredNode != null) {
_map[_hoveredNode].IsHovered = true;
m_textHoveredInfo.text = string.Format(CultureInfo.InvariantCulture, "<Hovered>\nZ: {2}, XY: ({0}, {1})\nD: {3}", node.X, node.Y, node.Z, node.Data.Id);
}
else {
m_textHoveredInfo.text = "";
}
}
void SelectNode(QuadTreeNode node) {
if (_selectedNode != null) {
_map[_selectedNode].IsSelected = false;
}
_selectedNode = node;
if (_selectedNode != null) {
_map[_selectedNode].IsSelected = true;
m_textSelectedInfo.text = string.Format(CultureInfo.InvariantCulture, "<Selected>\nZ: {2}, XY: ({0}, {1})\nD: {3}", node.X, node.Y, node.Z, node.Data.Id);
}
else {
m_textSelectedInfo.text = "";
}
}
void MergeNode(QuadTreeNode node) {
var parent = node.Parent;
if (parent == null)
return;
DestroyChildViews(parent);
_map[parent].IsLeaf = true;
parent.Merge();
_hoveredNode = null;
if (_selectedNode != null && !_map.ContainsKey(_selectedNode)) {
_selectedNode = null;
}
}
void SplitNode(QuadTreeNode node) {
node.Split();
_map[node].IsLeaf = false;
BuildChildViews(node);
}
static QuadTreeNode NewNode() => new() { Data = new("") };
sealed class QuadTreeNode {
QuadTreeNode[] m_children;
public QuadTreeNode[] Children {
get => m_children;
set {
if (m_children != null) {
foreach (var child in m_children) {
child.DetachFromParent();
}
}
m_children = value;
UpdateChildren();
}
}
QuadTreeNode m_parent;
[JsonIgnore] public QuadTreeNode Parent => m_parent;
void AttachToParent(QuadTreeNode parent, int index) {
if (m_parent != null && m_parent != parent)
throw new InvalidOperationException("Node already in a tree.");
m_parent = parent;
X = (parent.X << 1) | (index is 0 or 3 ? 1 : 0);
Y = (parent.Y << 1) | (index is 0 or 1 ? 1 : 0);
Z = parent.Z + 1;
UpdateChildren();
}
void DetachFromParent() => m_parent = null;
void UpdateChildren() {
if (m_children != null) {
for (int i = 0; i < m_children.Length; i++) {
m_children[i].AttachToParent(this, i);
}
}
}
[JsonIgnore] public int X { get; private set; }
[JsonIgnore] public int Y { get; private set; }
[JsonIgnore] public int Z { get; private set; }
public RegionData Data { get; set; }
public QuadTreeNode Get(Vector2 pos) {
if ((pos.x is < 0 or >= 1) || (pos.y is < 0 or >= 1))
return null;
if (m_children == null)
return this;
Vector2 subPos = pos * 2;
subPos.x %= 1;
subPos.y %= 1;
return pos.x >= 0.5f
? (pos.y >= 0.5f ? m_children[0] : m_children[3]).Get(subPos)
: (pos.y >= 0.5f ? m_children[1] : m_children[2]).Get(subPos);
}
public void Merge() {
Children = null;
}
public void Split() {
Children = new QuadTreeNode[] {
new() { Data = Data.Copy() },
new() { Data = Data.Copy() },
new() { Data = Data.Copy() },
new() { Data = Data.Copy() },
};
}
}
sealed record RegionData(string Id) {
public string Id { get; set; } = Id;
public RegionData Copy() => (RegionData)MemberwiseClone();
}
}
}

View File

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

View File

@@ -0,0 +1,68 @@
using UnityEngine;
namespace Cryville.EEW.Unity.Map {
sealed class RegionView : MonoBehaviour {
[SerializeField]
SpriteRenderer m_spriteRenderer;
Color _color;
bool m_isHovered;
public bool IsHovered {
get => m_isHovered;
set {
m_isHovered = value;
UpdateColor();
}
}
bool m_isSelected;
public bool IsSelected {
get => m_isSelected;
set {
m_isSelected = value;
UpdateColor();
}
}
bool m_isLeaf = true;
public bool IsLeaf {
get => m_isLeaf;
set {
m_isLeaf = value;
UpdateColor();
}
}
string m_id;
public string Id {
get => m_id;
set {
m_id = value;
unchecked {
uint hash = (uint)value.GetHashCode();
_color = Color.HSVToRGB(((hash >> 24) ^ ((hash >> 16) & 0xff) ^ ((hash >> 8) & 0xff) ^ (hash & 0xff)) / (float)0xff, 1, 1);
}
UpdateColor();
}
}
public void Init(int x, int y, int z) {
float scale = 1f / (1 << z);
transform.localScale = new Vector3(scale, scale, 1);
transform.localPosition = new Vector3(x * scale, y * scale - 1, -1 - z / 100f);
}
void UpdateColor() {
if (!m_isLeaf)
_color.a = 0.0f;
else if (m_isSelected)
_color.a = 0.6f;
else if (m_isHovered)
_color.a = 0.4f;
else
_color.a = 0.2f;
m_spriteRenderer.color = _color;
}
}
}

View File

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

View File

@@ -1,4 +1,5 @@
using Cryville.EEW.Colors;
using Cryville.EEW.Core;
using Cryville.EEW.Core.Colors;
using Cryville.EEW.FERegion;
using Cryville.EEW.Map;
@@ -25,8 +26,24 @@ namespace Cryville.EEW.Unity {
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 float HillshadeLayerOpacity { get; private set; } = 1;
public TimeSpan NowcastWarningDelayTolerance { get; private set; } = TimeSpan.FromMinutes(60);
public CultureInfo RVMCulture { get; private set; } = SharedCultures.CurrentUICulture;
readonly int _infoLocationSpecificity = 3;
readonly int _ttsLocationSpecificity = 3;
readonly LocationNamer _locationNamer = new() { Namer = new FERegionLongNamer() };
public bool NameLocation(double lat, double lon, CultureInfo localCulture, ref CultureInfo targetCulture, out string name, out int specificity) {
specificity = _ttsLocationSpecificity;
return _locationNamer.Name(lat, lon, localCulture, ref targetCulture, out name, ref specificity);
}
public bool NameLocation(double lat, double lon, CultureInfo localCulture, ref CultureInfo targetCulture, out string name) {
int specificity = _infoLocationSpecificity;
return _locationNamer.Name(lat, lon, localCulture, ref targetCulture, out name, ref specificity);
}
public IReadOnlyCollection<TTSCultureConfig> TTSCultures { get; private set; }
public bool DoIgnoreLanguageVariant { get; private set; }
public TimeZoneInfo OverrideTimeZone { get; private set; }
public bool DoDisplayTimeZone { get; private set; } = true;
@@ -101,9 +118,21 @@ namespace Cryville.EEW.Unity {
"SREV" => new DefaultTextColorScheme(Color.White, Color.FromArgb(28, 28, 28), 0.555f),
_ => new DefaultTextColorScheme(Color.White, Color.Black),
};
HillshadeLayerOpacity = config.HillshadeLayerOpacity;
_locationNamer.Namer = config.LocationNamer switch {
"FERegionShort" => new FERegionShortNamer(),
_ => new FERegionLongNamer(),
};
if (config.NowcastWarningDelayTolerance is string nowcastWarningDelayTolerance)
NowcastWarningDelayTolerance = TimeSpan.Parse(nowcastWarningDelayTolerance, CultureInfo.InvariantCulture);
OverrideTimeZone = ParseTimeZone(config.OverrideTimeZone);
DoDisplayTimeZone = config.DoDisplayTimeZone;
DoSwitchBackToHistory = config.DoSwitchBackToHistory;
RVMCulture = config.OverrideDisplayCulture is string rvmCulture
? (string.IsNullOrEmpty(rvmCulture) ? SharedCultures.CurrentUICulture : SharedCultures.Get(rvmCulture))
: CultureInfo.InvariantCulture;
TTSCultures = config.TTSCultures ?? new List<TTSCultureConfig> { new(CultureInfo.InvariantCulture) };
DoIgnoreLanguageVariant = config.DoIgnoreLanguageVariant;
EventSources = config.EventSources;
}

View File

@@ -5,7 +5,7 @@ using System.IO;
using UnityEngine;
namespace Cryville.EEW.Unity {
class SoundPlayer : Core.SoundPlayer {
class SoundPlayer : Core.Audio.SoundPlayer {
public SoundPlayer() : base(GetEngineList(), AudioUsage.NotificationEvent) { }
static List<Type> GetEngineList() => new() {
typeof(Audio.Wasapi.MMDeviceEnumeratorWrapper),
@@ -13,6 +13,7 @@ namespace Cryville.EEW.Unity {
};
protected override Stream Open(string path) {
App.MainLogger.Log(0, "Audio", null, "Opening audio file {0}", path);
path = Path.Combine(Application.streamingAssetsPath, "Sounds", path + ".ogg");
if (!File.Exists(path)) return null;
return new FileStream(path, FileMode.Open, FileAccess.Read);

View File

@@ -1,25 +1,53 @@
using SpeechLib;
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
namespace Cryville.EEW.Unity {
class TTSWorker : Core.TTSWorker {
public TTSWorker() : base(CreateSoundPlayer()) { }
class TTSWorker : Core.Audio.TTSWorker {
readonly ISpVoice _voice;
public TTSWorker() : base(CreateSoundPlayer()) {
App.MainLogger.Log(1, "Audio", null, "Initializing TTS worker");
try {
_voice = new SpVoiceClass();
}
catch { }
}
static SoundPlayer CreateSoundPlayer() {
App.MainLogger.Log(1, "Audio", null, "Creating sound player");
try {
return new SoundPlayer();
}
catch (InvalidOperationException) {
catch (InvalidOperationException ex) {
App.MainLogger.Log(3, "Audio", null, "An error occurred when creating sound player: {0}", ex);
return null;
}
}
protected override bool IsSpeaking() => false;
protected override bool IsSpeaking() {
if (_voice == null) return false;
_voice.GetStatus(out var status, out _);
return (status.dwRunningState & (uint)SpeechRunState.SRSEIsSpeaking) != 0;
}
protected override Task Speak(CultureInfo culture, string content, CancellationToken cancellationToken) => Task.CompletedTask;
protected override Task Speak(CultureInfo culture, string content, CancellationToken cancellationToken) {
if (_voice == null) return Task.CompletedTask;
_voice.Speak(
string.Format(CultureInfo.InvariantCulture, "<LANG LANGID=\"{0:x}\">{1}</LANG>", culture.LCID, content),
(uint)(SpeechVoiceSpeakFlags.SVSFlagsAsync | SpeechVoiceSpeakFlags.SVSFPurgeBeforeSpeak),
out _
);
App.MainLogger.Log(0, "Audio", null, "TTS ({0}): {1}", culture, content);
return Task.CompletedTask;
}
protected override void StopCurrent() { }
protected override void StopCurrent() {
if (_voice == null) return;
App.MainLogger.Log(0, "Audio", null, "TTS stopping current");
_voice.Skip("SENTENCE", int.MaxValue, out _);
}
}
}

Some files were not shown because too many files have changed in this diff Show More