2 Commits

Author SHA1 Message Date
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
11 changed files with 234 additions and 235 deletions

View File

@@ -15,14 +15,18 @@ namespace Cryville.EEW.Unity.Map {
MapElementManager m_layerElementSub;
[SerializeField]
GameObject m_prefabTile;
[SerializeField]
GameObject m_prefabBitmapHolder;
readonly MapTileCacheManager _tiles = new();
float _elementLayerZ;
void Start() {
_camera = GetComponent<Camera>();
_tiles.ExtraCachedZoomLevel = 20;
_tiles.Parent = m_layerTile;
_tiles.PrefabTile = m_prefabTile;
_tiles.PrefabBitmapHolder = m_prefabBitmapHolder;
_tiles.CacheDir = Application.temporaryCachePath;
_camera.orthographicSize = 0.5f / MathF.Max(1, (float)_camera.pixelWidth / _camera.pixelHeight);
_elementLayerZ = m_layerElement.transform.position.z;

View File

@@ -1,43 +1,21 @@
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);
@@ -45,69 +23,22 @@ namespace Cryville.EEW.Unity.Map {
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;
}
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);
}
}
}

View File

@@ -0,0 +1,35 @@
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 {
MapTileBitmapHolderBehaviour _behaviour;
public MapTileBitmapHolder(MapTileIndex index, GameObject gameObject) : base(index) {
_behaviour = gameObject.GetComponent<MapTileBitmapHolderBehaviour>();
}
protected override void Dispose(bool disposing) {
base.Dispose(disposing);
if (disposing) {
if (_behaviour) _behaviour.Destroy();
}
}
protected override Uri GetUri() =>
new($"https://server.arcgisonline.com/ArcGIS/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{Index.Z}/{Index.NY}/{Index.NX}");
protected override Task LoadBitmap(FileInfo file, CancellationToken cancellationToken) {
_behaviour.Load(file);
return Task.CompletedTask;
}
public void Bind(MapTile tile) {
_behaviour.Bind(tile);
}
}
}

View File

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

View File

@@ -0,0 +1,79 @@
using System;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
namespace Cryville.EEW.Unity.Map {
sealed class MapTileBitmapHolderBehaviour : MonoBehaviour {
Action<Sprite> _callback;
public void Bind(MapTile tile) {
if (_isDone)
tile.Set(_sprite);
else
_callback += tile.Set;
}
UnityWebRequest _req;
DownloadHandlerTexture _texHandler;
Texture2D _tex;
Sprite _sprite;
FileInfo _localFile;
bool _isReady;
bool _isDone;
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) {
Debug.LogException(ex);
}
_isReady = false;
}
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);
}
else {
Debug.LogError(_req.error);
_localFile.Delete();
}
_req.Dispose();
_texHandler.Dispose();
_req = null;
_callback?.Invoke(_sprite);
_isDone = true;
}
bool _isDestroyed;
public void Destroy() {
_isDestroyed = true;
}
void OnDestroy() {
if (_req != null) {
_req.Abort();
_req.Dispose();
_texHandler.Dispose();
}
if (_sprite) Destroy(_sprite);
if (_tex) Destroy(_tex);
}
}
}

View File

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

View File

@@ -1,165 +1,36 @@
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>();
}
}
sealed 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));
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);
uTile.Init(tile.Index);
tile.BitmapHolder.Bind(uTile);
}
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);
if (_map.TryGetValue(tile, out var uTile)) {
uTile.Destroy();
_map.Remove(tile);
}
}
}

View File

@@ -39,7 +39,8 @@ namespace Cryville.EEW.Unity {
[SerializeField] EventGroupListView m_historyEventGroupList;
[SerializeField] GameObject m_connectingHint;
GroupingCoreWorker _worker;
CoreWorker _worker;
ReportGrouper _grouper;
CancellationTokenSource _cancellationTokenSource;
void Awake() {
@@ -52,6 +53,7 @@ namespace Cryville.EEW.Unity {
App.Init();
_worker = new(new TTSWorker());
_grouper = new ReportGrouper();
_cancellationTokenSource = new();
}
@@ -64,8 +66,8 @@ namespace Cryville.EEW.Unity {
_worker.TTSMessageGeneratorContext = SharedSettings.Instance;
_ongoingReportManager.Changed += OnOngoingReported;
_worker.Reported += OnReported;
_worker.GroupUpdated += OnGroupUpdated;
_worker.GroupRemoved += OnGroupRemoved;
_grouper.GroupUpdated += OnGroupUpdated;
_grouper.GroupRemoved += OnGroupRemoved;
Task.Run(() => GatewayVerify(_cancellationTokenSource.Token)).ContinueWith(task => {
if (task.IsFaulted) {
OnReported(this, new() { Title = task.Exception.Message });
@@ -189,6 +191,7 @@ namespace Cryville.EEW.Unity {
void OnReported(object sender, ReportViewModel e) {
if (e.Model is Exception && e.Model is not SourceWorkerNetworkException)
Debug.LogError(e);
_grouper.Report(e);
_ongoingReportManager.Report(e);
_uiActionQueue.Enqueue(() => {
if (m_mapElementManager.OngoingCount == 0) {

View File

@@ -677,6 +677,7 @@ MonoBehaviour:
m_layerElement: {fileID: 1602500234}
m_layerElementSub: {fileID: 303098821}
m_prefabTile: {fileID: 7683017549812261837, guid: e090edd328c6750478f5849a43a9d278, type: 3}
m_prefabBitmapHolder: {fileID: 1455558857588368834, guid: 1a5cf693e0cf6b94390f72f521c151ba, type: 3}
--- !u!1 &408286580
GameObject:
m_ObjectHideFlags: 0

View File

@@ -0,0 +1,46 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1455558857588368834
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8746876075196617235}
- component: {fileID: 122704584488816298}
m_Layer: 0
m_Name: Bitmap Holder
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &8746876075196617235
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1455558857588368834}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &122704584488816298
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1455558857588368834}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c17f6b26e9a6bd74e8b2d071c6951c41, type: 3}
m_Name:
m_EditorClassIdentifier:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1a5cf693e0cf6b94390f72f521c151ba
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: