Enhance MapView component with new features and icons
- Added fullscreen and measurement mode buttons for improved map interaction. - Introduced a bookmarks section to save and navigate to locations easily. - Implemented a search feature for quick access to coordinates and markers. - Added keyboard shortcuts overlay for enhanced usability. - Refactored MapControls and MapControlsContent to support new functionalities. - Introduced new icon components for better visual representation in the UI.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full" @click="(e: MouseEvent) => e.button === 0 && mapLogic.closeContextMenus()">
|
||||
<div ref="mapContainerRef" class="relative h-full w-full" @click="(e: MouseEvent) => e.button === 0 && mapLogic.closeContextMenus()">
|
||||
<div
|
||||
v-if="!mapReady"
|
||||
class="absolute inset-0 z-[400] flex flex-col items-center justify-center gap-6 bg-base-200/95 p-8"
|
||||
@@ -32,6 +32,40 @@
|
||||
:mapid="mapLogic.state.mapid.value"
|
||||
:display-coords="mapLogic.state.displayCoords.value"
|
||||
/>
|
||||
<div class="absolute top-2 right-2 z-[501] flex flex-col gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm btn-square rounded-lg bg-base-100/95 backdrop-blur-sm border border-base-300/50 shadow hover:border-primary/50"
|
||||
:title="fullscreen.isFullscreen.value ? 'Exit fullscreen' : 'Fullscreen'"
|
||||
:aria-label="fullscreen.isFullscreen.value ? 'Exit fullscreen' : 'Fullscreen'"
|
||||
@click="fullscreen.toggle()"
|
||||
>
|
||||
<icons-icon-fullscreen-exit v-if="fullscreen.isFullscreen.value" class="size-4" />
|
||||
<icons-icon-fullscreen v-else class="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm btn-square rounded-lg bg-base-100/95 backdrop-blur-sm border border-base-300/50 shadow hover:border-primary/50"
|
||||
:class="{ 'border-primary bg-primary/10': measureMode }"
|
||||
:title="measureMode ? 'Exit measure mode' : 'Measure distance'"
|
||||
aria-label="Measure distance"
|
||||
@click="measureMode = !measureMode; if (!measureMode) clearMeasure()"
|
||||
>
|
||||
<icons-icon-ruler class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="measureMode && mapReady"
|
||||
class="absolute top-24 right-2 z-[500] rounded-lg px-3 py-2 text-sm bg-base-100/95 backdrop-blur-sm border border-base-300/50 shadow"
|
||||
>
|
||||
<template v-if="measurePointB == null">
|
||||
<span class="text-base-content/80">{{ measurePointA == null ? 'Click first point on map' : 'Click second point' }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="font-mono">Distance: {{ measureDistance.toFixed(2) }} units</span>
|
||||
<button type="button" class="btn btn-ghost btn-xs ml-2" @click="clearMeasure">Clear</button>
|
||||
</template>
|
||||
</div>
|
||||
<MapControls
|
||||
:show-grid-coordinates="mapLogic.state.showGridCoordinates.value"
|
||||
@update:show-grid-coordinates="(v) => (mapLogic.state.showGridCoordinates.value = v)"
|
||||
@@ -48,9 +82,12 @@
|
||||
:maps="maps"
|
||||
:quest-givers="questGivers"
|
||||
:players="players"
|
||||
:current-map-id="mapLogic.state.mapid.value"
|
||||
:current-coords="mapLogic.state.displayCoords.value"
|
||||
@zoom-in="mapLogic.zoomIn(leafletMap)"
|
||||
@zoom-out="mapLogic.zoomOutControl(leafletMap)"
|
||||
@reset-view="mapLogic.resetView(leafletMap)"
|
||||
@jump-to-marker="mapLogic.state.selectedMarkerId.value = $event"
|
||||
/>
|
||||
<MapContextMenu
|
||||
:context-menu="mapLogic.contextMenu"
|
||||
@@ -65,6 +102,7 @@
|
||||
@close="mapLogic.closeCoordSetModal()"
|
||||
@submit="onSubmitCoordSet"
|
||||
/>
|
||||
<MapShortcutsOverlay :open="showShortcutsOverlay" @close="showShortcutsOverlay = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -73,8 +111,11 @@ import MapControls from '~/components/map/MapControls.vue'
|
||||
import MapCoordsDisplay from '~/components/map/MapCoordsDisplay.vue'
|
||||
import MapContextMenu from '~/components/map/MapContextMenu.vue'
|
||||
import MapCoordSetModal from '~/components/map/MapCoordSetModal.vue'
|
||||
import MapShortcutsOverlay from '~/components/map/MapShortcutsOverlay.vue'
|
||||
import { HnHDefaultZoom, HnHMaxZoom, HnHMinZoom, TileSize } from '~/lib/LeafletCustomTypes'
|
||||
import { initLeafletMap, type MapInitResult } from '~/composables/useMapInit'
|
||||
import { useMapNavigate } from '~/composables/useMapNavigate'
|
||||
import { useFullscreen } from '~/composables/useFullscreen'
|
||||
import { startMapUpdates, type UseMapUpdatesReturn } from '~/composables/useMapUpdates'
|
||||
import { createMapLayers, type MapLayersManager } from '~/composables/useMapLayers'
|
||||
import type { MapInfo, ConfigResponse, MeResponse } from '~/types/api'
|
||||
@@ -91,10 +132,32 @@ const props = withDefaults(
|
||||
{ characterId: -1, mapId: undefined, gridX: 0, gridY: 0, zoom: 1 }
|
||||
)
|
||||
|
||||
const mapContainerRef = ref<HTMLElement | null>(null)
|
||||
const mapRef = ref<HTMLElement | null>(null)
|
||||
const api = useMapApi()
|
||||
const mapLogic = useMapLogic()
|
||||
const { resolvePath } = useAppPaths()
|
||||
const mapNavigate = useMapNavigate()
|
||||
const fullscreen = useFullscreen(mapContainerRef)
|
||||
const showShortcutsOverlay = ref(false)
|
||||
const measureMode = ref(false)
|
||||
const measurePointA = ref<{ x: number; y: number } | null>(null)
|
||||
const measurePointB = ref<{ x: number; y: number } | null>(null)
|
||||
const measureLayer = ref<L.LayerGroup | null>(null)
|
||||
const measureDistance = computed(() => {
|
||||
const a = measurePointA.value
|
||||
const b = measurePointB.value
|
||||
if (!a || !b) return 0
|
||||
return Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2)
|
||||
})
|
||||
|
||||
function clearMeasure() {
|
||||
measurePointA.value = null
|
||||
measurePointB.value = null
|
||||
if (measureLayer.value && leafletMap) {
|
||||
measureLayer.value.clearLayers()
|
||||
}
|
||||
}
|
||||
|
||||
/** Fallback marker icon (simple pin) when the real icon image fails to load. */
|
||||
const FALLBACK_MARKER_ICON =
|
||||
@@ -155,7 +218,35 @@ function onSubmitCoordSet(from: { x: number; y: number }, to: { x: number; y: nu
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') mapLogic.closeContextMenus()
|
||||
const target = e.target as HTMLElement
|
||||
const inInput = /^(INPUT|TEXTAREA|SELECT)$/.test(target?.tagName ?? '')
|
||||
if (e.key === 'Escape') {
|
||||
if (showShortcutsOverlay.value) showShortcutsOverlay.value = false
|
||||
else mapLogic.closeContextMenus()
|
||||
return
|
||||
}
|
||||
if (showShortcutsOverlay.value) return
|
||||
if (e.key === '?') {
|
||||
showShortcutsOverlay.value = true
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (inInput) return
|
||||
if (e.key === 'H') {
|
||||
mapLogic.state.hideMarkers.value = !mapLogic.state.hideMarkers.value
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (e.key === 'G') {
|
||||
mapLogic.state.showGridCoordinates.value = !mapLogic.state.showGridCoordinates.value
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (e.key === 'f' || e.key === 'F') {
|
||||
mapNavigate.focusSearch()
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -346,6 +437,15 @@ onMounted(async () => {
|
||||
if (value != null) mapLogic.state.trackingCharacterId.value = value
|
||||
})
|
||||
|
||||
mapNavigate.setGoTo((mapId: number, x: number, y: number, zoom?: number) => {
|
||||
if (mapLogic.state.mapid.value !== mapId && layersManager) {
|
||||
layersManager.changeMap(mapId)
|
||||
mapLogic.state.selectedMapId.value = mapId
|
||||
}
|
||||
const latLng = toLatLng(x * TileSize, y * TileSize)
|
||||
leafletMap!.setView(latLng, zoom ?? leafletMap!.getZoom(), { animate: true })
|
||||
})
|
||||
|
||||
leafletMap.on('moveend', () => mapLogic.updateDisplayCoords(leafletMap))
|
||||
mapLogic.updateDisplayCoords(leafletMap)
|
||||
leafletMap.on('zoomend', () => {
|
||||
@@ -361,12 +461,49 @@ onMounted(async () => {
|
||||
mapLogic.state.trackingCharacterId.value = -1
|
||||
}
|
||||
})
|
||||
|
||||
const measureLayerGroup = L.layerGroup().addTo(leafletMap)
|
||||
measureLayerGroup.setZIndex(550)
|
||||
measureLayer.value = measureLayerGroup
|
||||
leafletMap.on('click', (e: L.LeafletMouseEvent) => {
|
||||
if (!measureMode.value) return
|
||||
e.originalEvent.stopPropagation()
|
||||
const point = leafletMap!.project(e.latlng, HnHMaxZoom)
|
||||
const x = Math.floor(point.x / TileSize)
|
||||
const y = Math.floor(point.y / TileSize)
|
||||
if (measurePointA.value == null) {
|
||||
measurePointA.value = { x, y }
|
||||
} else if (measurePointB.value == null) {
|
||||
measurePointB.value = { x, y }
|
||||
} else {
|
||||
measurePointA.value = { x, y }
|
||||
measurePointB.value = null
|
||||
}
|
||||
})
|
||||
|
||||
watch([measurePointA, measurePointB], () => {
|
||||
const layer = measureLayer.value
|
||||
if (!layer) return
|
||||
layer.clearLayers()
|
||||
const a = measurePointA.value
|
||||
const b = measurePointB.value
|
||||
if (!a) return
|
||||
const latLngA = toLatLng(a.x * TileSize, a.y * TileSize)
|
||||
L.circleMarker(latLngA, { radius: 6, color: '#6366f1', fillColor: '#6366f1', fillOpacity: 0.8, weight: 2 }).addTo(layer)
|
||||
if (b) {
|
||||
const latLngB = toLatLng(b.x * TileSize, b.y * TileSize)
|
||||
L.circleMarker(latLngB, { radius: 6, color: '#6366f1', fillColor: '#6366f1', fillOpacity: 0.8, weight: 2 }).addTo(layer)
|
||||
L.polyline([latLngA, latLngB], { color: '#6366f1', weight: 2, opacity: 0.9 }).addTo(layer)
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (import.meta.client) {
|
||||
window.removeEventListener('keydown', onKeydown)
|
||||
}
|
||||
mapNavigate.setGoTo(null)
|
||||
if (contextMenuHandler) {
|
||||
document.removeEventListener('contextmenu', contextMenuHandler, true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user