815 lines
24 KiB
Vue
815 lines
24 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
ref,
|
|
shallowRef,
|
|
onBeforeUnmount,
|
|
onMounted,
|
|
watch,
|
|
nextTick,
|
|
} from 'vue'
|
|
import { loadModules } from 'esri-loader'
|
|
import axios from 'axios'
|
|
import { useCounterMixin } from '@/stores/mixin'
|
|
import { useQuasar } from 'quasar'
|
|
import { usePrivacyStore } from '@/stores/privacy'
|
|
|
|
const mixin = useCounterMixin()
|
|
const { messageError } = mixin
|
|
const privacyStore = usePrivacyStore()
|
|
|
|
const emit = defineEmits(['update:location'])
|
|
|
|
// Accept initial POI from parent to prevent skeleton on re-mount
|
|
const props = defineProps<{
|
|
initialPOI?: string
|
|
}>()
|
|
|
|
const $q = useQuasar()
|
|
|
|
// Throttle mechanism to prevent excessive location updates
|
|
const LOCATION_UPDATE_THROTTLE_MS = 500
|
|
let lastLocationUpdate = 0
|
|
|
|
// Track if map is initialized to prevent re-initialization
|
|
const isMapInitialized = ref<boolean>(false)
|
|
const isInitializing = ref<boolean>(false)
|
|
const currentScreenSize = ref<boolean>($q.screen.gt.xs) // Track screen size to detect changes
|
|
const mobileMapExpanded = ref<boolean>(true)
|
|
|
|
function updateLocation(latitude: number, longitude: number, namePOI: string) {
|
|
const now = Date.now()
|
|
// Skip update if called too frequently (within throttle period)
|
|
if (now - lastLocationUpdate < LOCATION_UPDATE_THROTTLE_MS) {
|
|
return
|
|
}
|
|
lastLocationUpdate = now
|
|
// ส่ง event ไปยัง parent component เพื่ออัพเดทค่า props
|
|
emit('update:location', latitude, longitude, namePOI)
|
|
}
|
|
|
|
const poiPlaceName = ref<string>(props.initialPOI || '') // ชื่อพื้นที่ใกล้เคียง - use initialPOI if provided
|
|
const mapView = shallowRef<any>(null) // Store mapView reference for cleanup (use shallowRef to avoid reactivity issues)
|
|
const desktopMapContainerRef = ref<HTMLElement | null>(null)
|
|
const mobileMapContainerRef = ref<HTMLElement | null>(null)
|
|
|
|
// Unique container ID for each component instance to avoid conflicts
|
|
const mapContainerId = `mapViewDisplay_${Math.random()
|
|
.toString(36)
|
|
.substring(2, 9)}`
|
|
|
|
// ArcGIS API key from environment variable
|
|
const apiKey = ref<string>(
|
|
'YLATgWuywoeRLHn6KImj5rg7UaP8bJoR9jiTldoCVBHlqFIebwMSA5wIXEmcYhwXwMHkmNISEYtUz3x0oiGIIx0bIXXnUwi0OzupoOEtDrQIsRPVtor7gaPpXEmH8TrNaMT3snf6zO_yujHLGzborg-L9aeAjTJn4ndL6f8qFmRzYcX93E2vyA-7XCufLYTRsdTE5Aq-9hnx1q9PmYVMqhAZpL7dWqn3JgO33fRXetk.'
|
|
)
|
|
const zoomMap = ref<number>(18)
|
|
const textTooltip = ref<string>(
|
|
'พื้นที่ใกล้เคียงคือ สถานที่สำคัญรอบตัวคุณ (ไม่ใช่ตำแหน่งปัจจุบัน)'
|
|
)
|
|
const MAP_DEBUG = true
|
|
let attachRetryTimer: number | undefined
|
|
let mapInitVersion = 0
|
|
let isRecoveryReinitializing = false
|
|
|
|
function logMapDebug(event: string, payload?: Record<string, unknown>) {
|
|
if (!MAP_DEBUG) {
|
|
return
|
|
}
|
|
|
|
const mode = $q.screen.gt.xs ? 'desktop' : 'mobile'
|
|
console.log('[AscGISMap]', event, {
|
|
mode,
|
|
isMapInitialized: isMapInitialized.value,
|
|
isInitializing: isInitializing.value,
|
|
hasMapView: !!mapView.value,
|
|
mapDestroyed: !!mapView.value?.destroyed,
|
|
...payload,
|
|
})
|
|
}
|
|
|
|
function getActiveMapContainer() {
|
|
return $q.screen.gt.xs
|
|
? desktopMapContainerRef.value
|
|
: mobileMapContainerRef.value
|
|
}
|
|
|
|
async function waitForActiveMapContainer(maxAttempts = 25, delayMs = 120) {
|
|
for (let i = 0; i < maxAttempts; i += 1) {
|
|
await nextTick()
|
|
const container = getActiveMapContainer()
|
|
if (i === 0 || i === maxAttempts - 1) {
|
|
logMapDebug('waitForContainer:attempt', {
|
|
attempt: i + 1,
|
|
maxAttempts,
|
|
width: container?.clientWidth || 0,
|
|
height: container?.clientHeight || 0,
|
|
})
|
|
}
|
|
|
|
if (container && container.clientWidth > 0 && container.clientHeight > 0) {
|
|
logMapDebug('waitForContainer:ready', {
|
|
attempt: i + 1,
|
|
width: container.clientWidth,
|
|
height: container.clientHeight,
|
|
})
|
|
return container
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
|
}
|
|
|
|
logMapDebug('waitForContainer:failed')
|
|
|
|
return null
|
|
}
|
|
|
|
function clearAttachRetryTimer() {
|
|
if (attachRetryTimer) {
|
|
clearTimeout(attachRetryTimer)
|
|
attachRetryTimer = undefined
|
|
}
|
|
}
|
|
|
|
async function recoverMapByReinitialize() {
|
|
if (isRecoveryReinitializing) {
|
|
logMapDebug('recover:skipAlreadyRunning')
|
|
return
|
|
}
|
|
|
|
isRecoveryReinitializing = true
|
|
logMapDebug('recover:start', { mapInitVersion })
|
|
|
|
try {
|
|
mapInitVersion += 1
|
|
isInitializing.value = false
|
|
clearAttachRetryTimer()
|
|
|
|
if (mapView.value && !mapView.value.destroyed) {
|
|
mapView.value.destroy()
|
|
mapView.value = null
|
|
}
|
|
|
|
isMapInitialized.value = false
|
|
await nextTick()
|
|
await initializeMap()
|
|
logMapDebug('recover:initializeMapCompleted', { mapInitVersion })
|
|
} finally {
|
|
isRecoveryReinitializing = false
|
|
logMapDebug('recover:end', { mapInitVersion })
|
|
}
|
|
}
|
|
|
|
function reattachAndResizeMap(retry = 0) {
|
|
if (!mapView.value || mapView.value.destroyed) {
|
|
logMapDebug('reattach:skipNoMapView', { retry })
|
|
return
|
|
}
|
|
|
|
const activeContainer = getActiveMapContainer()
|
|
|
|
if (
|
|
!activeContainer ||
|
|
activeContainer.clientWidth === 0 ||
|
|
activeContainer.clientHeight === 0
|
|
) {
|
|
logMapDebug('reattach:containerNotReady', {
|
|
retry,
|
|
width: activeContainer?.clientWidth || 0,
|
|
height: activeContainer?.clientHeight || 0,
|
|
})
|
|
if (retry < 15) {
|
|
clearAttachRetryTimer()
|
|
attachRetryTimer = window.setTimeout(() => {
|
|
reattachAndResizeMap(retry + 1)
|
|
}, 140)
|
|
} else if (!isRecoveryReinitializing) {
|
|
logMapDebug('reattach:triggerRecovery')
|
|
void recoverMapByReinitialize()
|
|
}
|
|
return
|
|
}
|
|
|
|
clearAttachRetryTimer()
|
|
|
|
nextTick(() => {
|
|
if (!mapView.value || mapView.value.destroyed) {
|
|
logMapDebug('reattach:skipDestroyedAfterTick', { retry })
|
|
return
|
|
}
|
|
|
|
if (mapView.value.container !== activeContainer) {
|
|
logMapDebug('reattach:rebindContainer', {
|
|
width: activeContainer.clientWidth,
|
|
height: activeContainer.clientHeight,
|
|
})
|
|
mapView.value.container = null
|
|
mapView.value.container = activeContainer
|
|
}
|
|
|
|
logMapDebug('reattach:resize', {
|
|
retry,
|
|
width: activeContainer.clientWidth,
|
|
height: activeContainer.clientHeight,
|
|
center: mapView.value.center,
|
|
zoom: mapView.value.zoom,
|
|
})
|
|
mapView.value.resize()
|
|
mapView.value.requestRender?.()
|
|
mapView.value
|
|
.goTo(
|
|
{
|
|
center: mapView.value.center,
|
|
zoom: mapView.value.zoom,
|
|
},
|
|
{ animate: false }
|
|
)
|
|
.catch(() => {
|
|
// Ignore interrupted goTo during rapid orientation changes.
|
|
})
|
|
})
|
|
}
|
|
|
|
async function initializeMap() {
|
|
if (isInitializing.value || (mapView.value && !mapView.value.destroyed)) {
|
|
logMapDebug('initialize:skipAlreadyRunningOrReady')
|
|
return
|
|
}
|
|
|
|
const initVersion = ++mapInitVersion
|
|
logMapDebug('initialize:start', { initVersion })
|
|
|
|
isInitializing.value = true
|
|
|
|
try {
|
|
// Load modules of ArcGIS
|
|
const [esriConfig, Map, MapView, Point, Graphic, TileLayer] =
|
|
await loadModules([
|
|
'esri/config',
|
|
'esri/Map',
|
|
'esri/views/MapView',
|
|
'esri/geometry/Point',
|
|
'esri/Graphic',
|
|
'esri/layers/TileLayer',
|
|
])
|
|
logMapDebug('initialize:modulesLoaded', { initVersion })
|
|
// Set apiKey
|
|
// esriConfig.apiKey =
|
|
// 'AAPK4f700a4324d04e9f8a1a134e0771ac45FXWawdCl-OotFfr52gz9XKxTDJTpDzw_YYcwbmKDDyAJswf14FoPyw0qBkN64DvP'
|
|
|
|
// Create a FeatureLayer using a custom server URL
|
|
// const hillshadeLayer = new TileLayer({
|
|
// url: `https://bmagis.bangkok.go.th/arcgis/rest/services/cache/BMA_3D_2D_Cache/MapServer`,
|
|
// })
|
|
|
|
const map = new Map({
|
|
basemap: 'streets',
|
|
// basemap: 'arcgis-topographic',
|
|
// layers: [hillshadeLayer],
|
|
})
|
|
|
|
if (initVersion !== mapInitVersion) {
|
|
logMapDebug('initialize:cancelledAfterMapCreated', {
|
|
initVersion,
|
|
mapInitVersion,
|
|
})
|
|
return
|
|
}
|
|
|
|
const position = await new Promise<GeolocationPosition>(
|
|
(resolve, reject) => {
|
|
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
|
enableHighAccuracy: true,
|
|
timeout: 10000,
|
|
})
|
|
}
|
|
)
|
|
|
|
if (initVersion !== mapInitVersion) {
|
|
logMapDebug('initialize:cancelledAfterGeolocation', {
|
|
initVersion,
|
|
mapInitVersion,
|
|
})
|
|
return
|
|
}
|
|
|
|
const { latitude, longitude } = position.coords
|
|
logMapDebug('initialize:geolocationSuccess', {
|
|
initVersion,
|
|
latitude,
|
|
longitude,
|
|
})
|
|
|
|
const mapContainer = await waitForActiveMapContainer()
|
|
|
|
if (!mapContainer || initVersion !== mapInitVersion) {
|
|
logMapDebug('initialize:containerUnavailableOrCancelled', {
|
|
initVersion,
|
|
mapInitVersion,
|
|
})
|
|
isMapInitialized.value = false
|
|
return
|
|
}
|
|
|
|
logMapDebug('initialize:containerReady', {
|
|
initVersion,
|
|
width: mapContainer.clientWidth,
|
|
height: mapContainer.clientHeight,
|
|
})
|
|
|
|
mapView.value = new MapView({
|
|
container: mapContainer,
|
|
map: map,
|
|
center: {
|
|
latitude: latitude,
|
|
longitude: longitude,
|
|
}, // Set the initial map center current position
|
|
|
|
zoom: zoomMap.value,
|
|
constraints: {
|
|
snapToZoom: false, // Disables snapping to the zoom level
|
|
minZoom: zoomMap.value, // Set minimum zoom level
|
|
maxZoom: zoomMap.value, // Set maximum zoom level (same as minZoom for fixed zoom)
|
|
},
|
|
|
|
ui: {
|
|
components: [], // Empty array to remove all default UI components
|
|
},
|
|
})
|
|
|
|
await mapView.value.when()
|
|
logMapDebug('initialize:mapViewReady', { initVersion })
|
|
|
|
if (initVersion !== mapInitVersion) {
|
|
logMapDebug('initialize:cancelledAfterMapViewReady', {
|
|
initVersion,
|
|
mapInitVersion,
|
|
})
|
|
if (mapView.value && !mapView.value.destroyed) {
|
|
mapView.value.destroy()
|
|
}
|
|
mapView.value = null
|
|
return
|
|
}
|
|
|
|
isMapInitialized.value = true
|
|
reattachAndResizeMap()
|
|
|
|
// ตำแหน่งของผู้ใช้
|
|
const userPoint = new Point({ longitude, latitude })
|
|
const userSymbol = {
|
|
type: 'picture-marker',
|
|
url: 'http://maps.google.com/mapfiles/ms/icons/red.png',
|
|
width: '32px',
|
|
height: '32px',
|
|
}
|
|
const userGraphic = new Graphic({
|
|
geometry: userPoint,
|
|
symbol: userSymbol,
|
|
})
|
|
mapView.value.graphics.add(userGraphic)
|
|
// Get POI place ยิงไปขอที่ server ของกทม.ก่อน
|
|
// await axios
|
|
// .get(
|
|
// 'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/',
|
|
// {
|
|
// params: {
|
|
// f: 'json', // Format JSON response
|
|
// distance: 2000,
|
|
// category: 'POI',
|
|
// location: {
|
|
// spatialReference: { wkid: 4326 },
|
|
// x: longitude,
|
|
// y: latitude,
|
|
// },
|
|
// token: apiKey.value,
|
|
// },
|
|
// }
|
|
// )
|
|
// .then((response) => {
|
|
// // console.log('poi', response.data.location)
|
|
// poiPlaceName.value = response.data.address
|
|
// ? response.data.address.PlaceName === ''
|
|
// ? response.data.address.ShortLabel
|
|
// : response.data.address.PlaceName
|
|
// : 'ไม่พบข้อมูล'
|
|
// const poiPoint = new Point({
|
|
// longitude: response.data.location.x,
|
|
// latitude: response.data.location.y,
|
|
// })
|
|
// const poiSymbol = {
|
|
// type: 'picture-marker',
|
|
// url: 'http://maps.google.com/mapfiles/ms/icons/blue.png',
|
|
// width: '32px',
|
|
// height: '32px',
|
|
// }
|
|
// const poiGraphic = new Graphic({
|
|
// geometry: poiPoint,
|
|
// symbol: poiSymbol,
|
|
// })
|
|
// mapView.value.graphics.add(poiGraphic)
|
|
// // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI
|
|
// mapView.value.goTo({
|
|
// target: [userPoint, poiPoint],
|
|
// zoom: zoomMap.value,
|
|
// })
|
|
|
|
// // Mark map as initialized
|
|
// isMapInitialized.value = true
|
|
|
|
// updateLocation(latitude, longitude, poiPlaceName.value)
|
|
// })
|
|
// .catch(async (error) => {
|
|
// console.error('Error fetching points of interest:', error)
|
|
// Get POI place ยิงไปขอที่ server arcgis ไม่ต้องใช้ token
|
|
await axios
|
|
.get(
|
|
'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/',
|
|
{
|
|
params: {
|
|
f: 'json', // Format JSON response
|
|
distance: 2000,
|
|
category: 'POI',
|
|
location: {
|
|
spatialReference: { wkid: 4326 },
|
|
x: longitude,
|
|
y: latitude,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
.then((response) => {
|
|
if (initVersion !== mapInitVersion || !mapView.value) {
|
|
logMapDebug('initialize:poiIgnoredByVersionOrMap', {
|
|
initVersion,
|
|
mapInitVersion,
|
|
})
|
|
return
|
|
}
|
|
|
|
// console.log('poi', response.data.location)
|
|
poiPlaceName.value = response.data.address
|
|
? response.data.address.PlaceName === ''
|
|
? response.data.address.ShortLabel
|
|
: response.data.address.PlaceName
|
|
: 'ไม่พบข้อมูล'
|
|
const poiPoint = new Point({
|
|
longitude: response.data.location.x,
|
|
latitude: response.data.location.y,
|
|
})
|
|
const poiSymbol = {
|
|
type: 'picture-marker',
|
|
url: 'http://maps.google.com/mapfiles/ms/icons/blue.png',
|
|
width: '32px',
|
|
height: '32px',
|
|
}
|
|
const poiGraphic = new Graphic({
|
|
geometry: poiPoint,
|
|
symbol: poiSymbol,
|
|
})
|
|
mapView.value.graphics.add(poiGraphic)
|
|
// อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI
|
|
mapView.value.goTo({
|
|
target: [userPoint, poiPoint],
|
|
zoom: zoomMap.value,
|
|
})
|
|
|
|
updateLocation(latitude, longitude, poiPlaceName.value)
|
|
logMapDebug('initialize:poiSuccess', {
|
|
initVersion,
|
|
poiPlaceName: poiPlaceName.value,
|
|
})
|
|
})
|
|
.catch(() => {
|
|
if (initVersion !== mapInitVersion) {
|
|
logMapDebug('initialize:poiErrorIgnoredByVersion', {
|
|
initVersion,
|
|
mapInitVersion,
|
|
})
|
|
return
|
|
}
|
|
poiPlaceName.value = poiPlaceName.value || 'ไม่พบข้อมูล'
|
|
updateLocation(latitude, longitude, poiPlaceName.value)
|
|
logMapDebug('initialize:poiErrorFallback', {
|
|
initVersion,
|
|
poiPlaceName: poiPlaceName.value,
|
|
})
|
|
})
|
|
|
|
if (initVersion === mapInitVersion) {
|
|
reattachAndResizeMap()
|
|
}
|
|
} catch (error) {
|
|
if (initVersion === mapInitVersion) {
|
|
isMapInitialized.value = false
|
|
}
|
|
logMapDebug('initialize:error', {
|
|
initVersion,
|
|
mapInitVersion,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
})
|
|
console.error('Error loading the map', error)
|
|
} finally {
|
|
if (initVersion === mapInitVersion) {
|
|
isInitializing.value = false
|
|
}
|
|
logMapDebug('initialize:end', {
|
|
initVersion,
|
|
mapInitVersion,
|
|
isMapInitialized: isMapInitialized.value,
|
|
})
|
|
}
|
|
}
|
|
|
|
const locationGranted = ref<boolean>(false)
|
|
// Function to request location permission
|
|
const requestLocationPermission = () => {
|
|
// เช็คสิทธิ์ privacy ก่อนเข้าถึงแผนที่
|
|
if (!privacyStore.isAccepted) {
|
|
$q.notify({
|
|
type: 'warning',
|
|
message: 'กรุณายอมรับนโยบายคุ้มครองข้อมูลส่วนบุคคลก่อนใช้งานแผนที่',
|
|
position: 'top',
|
|
})
|
|
return
|
|
}
|
|
|
|
if (!navigator.geolocation) {
|
|
messageError(
|
|
$q,
|
|
'',
|
|
'ไม่สามารถระบุตำแหน่งปัจจุบันได้ เบราว์เซอร์ของคุณไม่รองรับ Geolocation'
|
|
)
|
|
return
|
|
}
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
async (position) => {
|
|
// Permission granted
|
|
locationGranted.value = true
|
|
|
|
const { latitude, longitude } = position.coords
|
|
// console.log('Current position:', latitude, longitude)
|
|
|
|
if (!latitude || !longitude) {
|
|
messageError($q, '', 'ไม่สามารถระบุตำแหน่งปัจจุบันได้')
|
|
return
|
|
}
|
|
|
|
// Center map on user's location if map is initialized
|
|
if (privacyStore.isAccepted) {
|
|
await initializeMap()
|
|
}
|
|
},
|
|
(error) => {
|
|
// Permission denied
|
|
locationGranted.value = false
|
|
|
|
switch (error.code) {
|
|
case error.PERMISSION_DENIED:
|
|
messageError(
|
|
$q,
|
|
'',
|
|
'ไม่สามารถระบุตำแหน่งปัจจุบันได้ เนื่องจากคุณปฏิเสธการเข้าถึงตำแหน่ง กรุณาเปิดการเข้าถึงตำแหน่ง'
|
|
)
|
|
break
|
|
case error.POSITION_UNAVAILABLE:
|
|
messageError($q, '', 'ไม่สามารถระบุตำแหน่งปัจจุบันได้')
|
|
break
|
|
case error.TIMEOUT:
|
|
messageError($q, '', 'การร้องขอตำแหน่งหมดเวลา กรุณาลองอีกครั้ง')
|
|
break
|
|
default:
|
|
messageError(
|
|
$q,
|
|
'',
|
|
'ไม่สามารถระบุตำแหน่งปัจจุบันได้ เกิดข้อผิดพลาดไม่ทราบสาเหตุในการเข้าถึงตำแหน่ง'
|
|
)
|
|
break
|
|
}
|
|
},
|
|
{ enableHighAccuracy: true }
|
|
)
|
|
}
|
|
|
|
// Also add a resize event listener as a fallback for orientation changes
|
|
// that might not trigger the Quasar screen watcher
|
|
let resizeTimeout: number | undefined
|
|
const handleWindowResize = () => {
|
|
// Debounce resize events to avoid excessive calls
|
|
if (resizeTimeout) {
|
|
clearTimeout(resizeTimeout)
|
|
}
|
|
resizeTimeout = window.setTimeout(() => {
|
|
handleMapResize()
|
|
}, 300)
|
|
}
|
|
|
|
/**
|
|
* Handle map resize when screen orientation changes
|
|
* ArcGIS MapView needs to be manually resized when its container changes dimensions
|
|
* Only call resize() if the map is fully initialized
|
|
*/
|
|
function handleMapResize() {
|
|
logMapDebug('handleMapResize:called')
|
|
if (mapView.value && !mapView.value.destroyed && isMapInitialized.value) {
|
|
// Use nextTick to ensure the DOM has finished updating before resizing
|
|
reattachAndResizeMap()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Watch for screen size/orientation changes
|
|
* When the screen size changes (e.g., portrait to landscape), we need to re-initialize the map
|
|
* because the container div changes between desktop and mobile versions
|
|
*/
|
|
watch(
|
|
() => $q.screen.gt.xs,
|
|
async (newValue) => {
|
|
// Skip if no actual change (same layout)
|
|
if (newValue === currentScreenSize.value) {
|
|
logMapDebug('screenWatch:skipNoChange', { newValue })
|
|
return
|
|
}
|
|
|
|
logMapDebug('screenWatch:changed', {
|
|
oldValue: currentScreenSize.value,
|
|
newValue,
|
|
})
|
|
currentScreenSize.value = newValue
|
|
if (!newValue) {
|
|
mobileMapExpanded.value = true
|
|
}
|
|
mapInitVersion += 1
|
|
isInitializing.value = false
|
|
clearAttachRetryTimer()
|
|
|
|
// Always destroy and re-initialize when screen size changes
|
|
// This ensures the map is attached to the correct container
|
|
if (mapView.value && !mapView.value.destroyed) {
|
|
logMapDebug('screenWatch:destroyMapView')
|
|
mapView.value.destroy()
|
|
mapView.value = null
|
|
}
|
|
|
|
// Reset initialization state
|
|
isMapInitialized.value = false
|
|
|
|
// Wait for DOM to update with new layout
|
|
await nextTick()
|
|
await waitForActiveMapContainer(30, 100)
|
|
|
|
// Re-initialize the map
|
|
if (privacyStore.isAccepted) {
|
|
await initializeMap()
|
|
|
|
if (!mapView.value || mapView.value.destroyed) {
|
|
logMapDebug('screenWatch:missingMapAfterInitialize, triggerRecovery')
|
|
await recoverMapByReinitialize()
|
|
}
|
|
|
|
reattachAndResizeMap()
|
|
logMapDebug('screenWatch:reinitialized', {
|
|
hasMapViewAfterReinit: !!mapView.value,
|
|
mapDestroyedAfterReinit: !!mapView.value?.destroyed,
|
|
})
|
|
}
|
|
}
|
|
)
|
|
|
|
// Watch for initialPOI prop changes to update poiPlaceName
|
|
watch(
|
|
() => props.initialPOI,
|
|
(newValue) => {
|
|
if (newValue && newValue !== poiPlaceName.value) {
|
|
poiPlaceName.value = newValue
|
|
}
|
|
}
|
|
)
|
|
|
|
// Set up resize event listener on mount
|
|
onMounted(async () => {
|
|
logMapDebug('lifecycle:mounted')
|
|
window.addEventListener('resize', handleWindowResize)
|
|
|
|
// Component can be remounted after orientation/layout switches.
|
|
// Recreate map automatically when consent is already granted.
|
|
if (privacyStore.isAccepted && (!mapView.value || mapView.value.destroyed)) {
|
|
logMapDebug('lifecycle:mounted:triggerInitialize')
|
|
await waitForActiveMapContainer(30, 100)
|
|
await initializeMap()
|
|
reattachAndResizeMap()
|
|
} else {
|
|
logMapDebug('lifecycle:mounted:skipInitialize', {
|
|
privacyAccepted: privacyStore.isAccepted,
|
|
hasMapView: !!mapView.value,
|
|
mapDestroyed: !!mapView.value?.destroyed,
|
|
})
|
|
}
|
|
})
|
|
|
|
// Cleanup map resources when component unmounts
|
|
onBeforeUnmount(() => {
|
|
logMapDebug('lifecycle:beforeUnmount')
|
|
clearAttachRetryTimer()
|
|
|
|
if (mapView.value) {
|
|
mapView.value.destroy()
|
|
mapView.value = null
|
|
}
|
|
// Clean up resize event listener
|
|
window.removeEventListener('resize', handleWindowResize)
|
|
// Clear timeout if it exists
|
|
if (resizeTimeout) {
|
|
clearTimeout(resizeTimeout)
|
|
}
|
|
})
|
|
|
|
defineExpose({
|
|
requestLocationPermission,
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Loading skeleton - show while map is initializing -->
|
|
<div v-if="isInitializing && !isMapInitialized" class="col-12">
|
|
<q-skeleton
|
|
:height="$q.screen.gt.xs ? '35vh' : '45px'"
|
|
width="100%"
|
|
:style="$q.screen.gt.xs ? ';' : 'border-radius: 20px'"
|
|
class="bg-grey-4"
|
|
/>
|
|
</div>
|
|
|
|
<q-card
|
|
bordered
|
|
flat
|
|
class="col-12 bg-grey-2 shadow-0"
|
|
:style="$q.screen.gt.xs ? ';' : 'border-radius: 20px'"
|
|
>
|
|
<div v-if="$q.screen.gt.xs">
|
|
<div
|
|
:id="mapContainerId"
|
|
ref="desktopMapContainerRef"
|
|
style="height: 35vh; pointer-events: none"
|
|
></div>
|
|
|
|
<div
|
|
:class="
|
|
$q.screen.gt.xs
|
|
? 'q-pa-xs text-weight-medium text-grey-8'
|
|
: 'q-pa-xs text-weight-medium text-grey-8'
|
|
"
|
|
>
|
|
พื้นที่ใกล้เคียง
|
|
<q-icon name="mdi-information-outline" size="xs" class="q-mr-xs" />
|
|
|
|
<q-tooltip anchor="top middle" self="bottom middle" :offset="[0, 5]">
|
|
{{ textTooltip }}
|
|
</q-tooltip>
|
|
<span class="q-px-sm">:</span>
|
|
{{ poiPlaceName }}
|
|
</div>
|
|
</div>
|
|
|
|
<q-card v-else style="border-radius: 20px">
|
|
<q-expansion-item
|
|
v-model="mobileMapExpanded"
|
|
class="shadow-1 overflow-hidden bg-grey-4 text-left q-pa-xs"
|
|
style="border-radius: 20px"
|
|
dense
|
|
default-opened
|
|
>
|
|
<template v-slot:header>
|
|
<q-item-section avatar class="q-pr-none expanAS">
|
|
<q-icon name="mdi-map-marker" color="primary" />
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label caption class="text-grey-7">
|
|
พื้นที่ใกล้เคียง
|
|
<q-icon
|
|
name="mdi-information-outline"
|
|
size="xs"
|
|
class="q-mr-xs cursor-pointer"
|
|
@click.stop="$q.dialog({ message: textTooltip, ok: 'ตกลง' })"
|
|
/>
|
|
</q-item-label>
|
|
<q-item-label class="text-weight-medium text-grey-9">
|
|
{{ poiPlaceName || '-' }}
|
|
</q-item-label>
|
|
</q-item-section>
|
|
</template>
|
|
|
|
<div
|
|
:id="mapContainerId"
|
|
ref="mobileMapContainerRef"
|
|
style="height: 20vh"
|
|
></div>
|
|
</q-expansion-item>
|
|
</q-card>
|
|
</q-card>
|
|
</template>
|
|
|
|
<style>
|
|
.expanAS.q-item__section--avatar {
|
|
min-width: 40px !important;
|
|
}
|
|
</style>
|