From 225aaa36e780d4203dedab8879bb818859b2642b Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Sun, 1 Mar 2026 19:09:46 +0300 Subject: [PATCH] Enhance map functionality and API documentation - Updated API documentation for the `rebuildZooms` endpoint to clarify its long execution time and response behavior. - Modified MapView component to manage tile cache invalidation after rebuilding zoom levels, ensuring fresh tile display. - Introduced a new composable for handling tile cache invalidation state after admin actions. - Enhanced character icon creation to reflect ownership status with distinct colors. - Improved loading state handling in various components for better user experience during data fetching. --- docs/api.md | 2 +- frontend-nuxt/components/MapView.vue | 50 ++++++++++++++++--- .../components/map/MapControlsContent.vue | 7 ++- frontend-nuxt/composables/useMapApi.ts | 2 +- frontend-nuxt/composables/useMapInit.ts | 6 +-- .../useRebuildZoomsInvalidation.ts | 30 +++++++++++ frontend-nuxt/layouts/default.vue | 15 ++++-- frontend-nuxt/lib/Character.ts | 39 +++++++++++---- frontend-nuxt/lib/LeafletCustomTypes.ts | 8 +-- frontend-nuxt/lib/__tests__/Character.test.ts | 36 ++++++++++--- frontend-nuxt/lib/characterColors.ts | 48 ++++++++++++++++++ frontend-nuxt/pages/admin/index.vue | 3 ++ internal/app/handlers/admin.go | 5 +- internal/app/services/admin.go | 4 +- internal/app/services/export.go | 3 +- internal/app/services/map.go | 30 ++++++++--- 16 files changed, 236 insertions(+), 52 deletions(-) create mode 100644 frontend-nuxt/composables/useRebuildZoomsInvalidation.ts create mode 100644 frontend-nuxt/lib/characterColors.ts diff --git a/docs/api.md b/docs/api.md index 24c3af2..34c37fb 100644 --- a/docs/api.md +++ b/docs/api.md @@ -40,7 +40,7 @@ The API is available under the `/map/api/` prefix. Requests requiring authentica - **POST /map/api/admin/maps/:id** — update a map (name, hidden, priority). - **POST /map/api/admin/maps/:id/toggle-hidden** — toggle map visibility. - **POST /map/api/admin/wipe** — wipe grids, markers, tiles, and maps from the database. -- **POST /map/api/admin/rebuildZooms** — rebuild tile zoom levels. +- **POST /map/api/admin/rebuildZooms** — rebuild tile zoom levels from base tiles. The operation can take a long time (minutes) when there are many grids; the client should allow for request timeouts or show appropriate loading state. On success returns 200; on failure (e.g. store error) returns 500. - **GET /map/api/admin/export** — download data export (ZIP). - **POST /map/api/admin/merge** — upload and apply a merge (ZIP with grids and markers). - **GET /map/api/admin/wipeTile** — delete a tile. Query: `map`, `x`, `y`. diff --git a/frontend-nuxt/components/MapView.vue b/frontend-nuxt/components/MapView.vue index 706522e..0e29803 100644 --- a/frontend-nuxt/components/MapView.vue +++ b/frontend-nuxt/components/MapView.vue @@ -195,6 +195,8 @@ let intervalId: ReturnType | null = null let autoMode = false let mapContainer: HTMLElement | null = null let contextMenuHandler: ((ev: MouseEvent) => void) | null = null +let mounted = false +let visibilityChangeHandler: (() => void) | null = null function toLatLng(x: number, y: number) { return leafletMap!.unproject([x, y], HnHMaxZoom) @@ -264,6 +266,7 @@ function onKeydown(e: KeyboardEvent) { } onMounted(async () => { + mounted = true if (import.meta.client) { window.addEventListener('keydown', onKeydown) } @@ -353,9 +356,11 @@ onMounted(async () => { getCurrentMapId: () => mapLogic.state.mapid.value, connectionStateRef: sseConnectionState, onMerge: (mapTo: number, shift: { x: number; y: number }) => { + if (!mounted) return const latLng = toLatLng(shift.x * 100, shift.y * 100) layersManager!.changeMap(mapTo) api.getMarkers().then((body) => { + if (!mounted) return layersManager!.updateMarkers(Array.isArray(body) ? body : []) questGivers.value = layersManager!.getQuestGivers() }) @@ -363,6 +368,17 @@ onMounted(async () => { }, }) + const { shouldInvalidateTileCache, clearRebuildDoneFlag } = useRebuildZoomsInvalidation() + if (shouldInvalidateTileCache()) { + mapInit.layer.cache = {} + mapInit.overlayLayer.cache = {} + clearRebuildDoneFlag() + nextTick(() => { + mapInit.layer.redraw() + mapInit.overlayLayer.redraw() + }) + } + const charsList = Array.isArray(charactersData) ? charactersData : [] layersManager.updateCharacters(charsList) players.value = layersManager.getPlayers() @@ -395,6 +411,7 @@ onMounted(async () => { // Markers load asynchronously after map is visible. api.getMarkers().then((body) => { + if (!mounted) return layersManager!.updateMarkers(Array.isArray(body) ? body : []) questGivers.value = layersManager!.getQuestGivers() }) @@ -406,6 +423,7 @@ onMounted(async () => { api .getCharacters() .then((body) => { + if (!mounted) return const list = Array.isArray(body) ? body : [] layersManager!.updateCharacters(list) players.value = layersManager!.getPlayers() @@ -429,17 +447,31 @@ onMounted(async () => { startCharacterPoll() if (import.meta.client) { - document.addEventListener('visibilitychange', () => { + visibilityChangeHandler = () => { startCharacterPoll() - }) + } + document.addEventListener('visibilitychange', visibilityChangeHandler) } watch(mapLogic.state.showGridCoordinates, (v) => { - if (mapInit?.coordLayer) { - ;(mapInit.coordLayer.options as { visible?: boolean }).visible = v - mapInit.coordLayer.setOpacity(v ? 1 : 0) - mapInit.coordLayer.redraw?.() - if (v) mapInit.coordLayer.bringToFront?.() + if (!mapInit?.coordLayer || !leafletMap) return + const coordLayer = mapInit.coordLayer + const layerWithMap = coordLayer as L.GridLayer & { _map?: L.Map } + if (v) { + ;(coordLayer.options as { visible?: boolean }).visible = true + if (!layerWithMap._map) { + coordLayer.addTo(leafletMap) + coordLayer.setZIndex(500) + coordLayer.setOpacity(1) + coordLayer.bringToFront?.() + } + leafletMap.invalidateSize() + nextTick(() => { + coordLayer.redraw?.() + }) + } else { + coordLayer.setOpacity(0) + coordLayer.remove() } }) @@ -553,8 +585,12 @@ onMounted(async () => { }) onBeforeUnmount(() => { + mounted = false if (import.meta.client) { window.removeEventListener('keydown', onKeydown) + if (visibilityChangeHandler) { + document.removeEventListener('visibilitychange', visibilityChangeHandler) + } } mapNavigate.setGoTo(null) if (contextMenuHandler) { diff --git a/frontend-nuxt/components/map/MapControlsContent.vue b/frontend-nuxt/components/map/MapControlsContent.vue index 3a22f78..31c91f5 100644 --- a/frontend-nuxt/components/map/MapControlsContent.vue +++ b/frontend-nuxt/components/map/MapControlsContent.vue @@ -56,7 +56,12 @@ Display