fix switch Landscape -> Portrait

This commit is contained in:
Warunee Tamkoo 2026-04-30 15:55:31 +07:00
parent ec22714f01
commit e9d28197df
5 changed files with 964 additions and 279 deletions

View file

@ -0,0 +1,5 @@
# HRMS Check-in Expert Memory
## Project Issues & Fixes
- [Position Orientation Change Fix](issue_position_orientation_change_fix.md) - Fix for position map not displaying on screen orientation changes

View file

@ -0,0 +1,48 @@
---
name: position_orientation_change_fix
description: Fix for position map not displaying on screen orientation changes
type: project
---
## Issue: Position/Geolocation Not Displaying on Screen Orientation Changes
**Problem:**
The ArcGIS map component (`AscGISMap.vue`) failed to display or update position when the screen orientation changed between portrait and landscape modes.
**Root Cause:**
1. The component uses `v-if="$q.screen.gt.xs"` for conditional rendering of different layouts
2. When orientation changes, the ArcGIS MapView doesn't automatically resize to fit the new container dimensions
3. ArcGIS MapView requires manual `resize()` call when its container's size changes
**Solution Implemented:**
Added screen resize handling to `src/components/AscGISMap.vue`:
1. **Added state tracking:**
- `isMapInitialized` ref to track when map is ready
- `isInitializing` ref to prevent concurrent initializations
2. **Added `handleMapResize()` function:**
- Checks if mapView exists and isn't destroyed
- Uses `nextTick()` to ensure DOM updates complete before resizing
- Calls `mapView.value.resize()` to update map dimensions
3. **Added watcher on `$q.screen.gt.xs`:**
- Triggers when screen size crosses the xs breakpoint
- Waits for DOM update via `nextTick()`
- Calls `handleMapResize()` to adjust map
4. **Added window resize event listener:**
- Falls back for orientation changes that might not trigger Quasar's screen watcher
- Debounced with 300ms timeout to avoid excessive calls
- Properly cleaned up on component unmount
**Files Modified:**
- `src/components/AscGISMap.vue` - Added resize handling logic
**Best Practices Applied:**
- Proper cleanup of event listeners in `onBeforeUnmount`
- Debouncing resize events for performance
- Using `nextTick()` to ensure DOM synchronization
- Checking for destroyed state before calling map methods
**Note:** The Google Maps component (`MapCheckin.vue`) was not affected as `vue3-google-map` handles resize automatically.

View file

@ -1,5 +1,12 @@
<script setup lang="ts">
import { ref, shallowRef, onBeforeUnmount } from 'vue'
import {
ref,
shallowRef,
onBeforeUnmount,
onMounted,
watch,
nextTick,
} from 'vue'
import { loadModules } from 'esri-loader'
import axios from 'axios'
import { useCounterMixin } from '@/stores/mixin'
@ -11,12 +18,24 @@ 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)
@ -28,8 +47,15 @@ function updateLocation(latitude: number, longitude: number, namePOI: string) {
emit('update:location', latitude, longitude, namePOI)
}
const poiPlaceName = ref<string>('') //
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>(
@ -39,174 +65,457 @@ 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
loadModules([
'esri/config',
'esri/Map',
'esri/views/MapView',
'esri/geometry/Point',
'esri/Graphic',
'esri/layers/TileLayer',
]).then(async ([esriConfig, Map, MapView, Point, Graphic, TileLayer]) => {
// Set apiKey
// esriConfig.apiKey =
// 'AAPK4f700a4324d04e9f8a1a134e0771ac45FXWawdCl-OotFfr52gz9XKxTDJTpDzw_YYcwbmKDDyAJswf14FoPyw0qBkN64DvP'
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`,
// })
// 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],
const map = new Map({
basemap: 'streets',
// basemap: 'arcgis-topographic',
// layers: [hillshadeLayer],
})
if (initVersion !== mapInitVersion) {
logMapDebug('initialize:cancelledAfterMapCreated', {
initVersion,
mapInitVersion,
})
return
}
navigator.geolocation.getCurrentPosition(async (position) => {
const { latitude, longitude } = position.coords
mapView.value = new MapView({
container: 'mapViewDisplay',
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
},
const position = await new Promise<GeolocationPosition>(
(resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000,
})
}
)
//
const userPoint = new Point({ longitude, latitude })
const userSymbol = {
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/red.png',
url: 'http://maps.google.com/mapfiles/ms/icons/blue.png',
width: '32px',
height: '32px',
}
const userGraphic = new Graphic({
geometry: userPoint,
symbol: userSymbol,
const poiGraphic = new Graphic({
geometry: poiPoint,
symbol: poiSymbol,
})
mapView.value.graphics.add(poiGraphic)
// POI
mapView.value.goTo({
target: [userPoint, poiPoint],
zoom: zoomMap.value,
})
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,
})
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) => {
// 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)
})
.catch((error) => {
// console.error('Error fetching points of interest:', error)
})
})
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,
})
}
}
@ -281,12 +590,137 @@ const requestLocationPermission = () => {
)
}
// 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({
@ -295,8 +729,8 @@ defineExpose({
</script>
<template>
<!-- Loading skeleton -->
<div v-if="!poiPlaceName" class="col-12">
<!-- 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%"
@ -306,7 +740,6 @@ defineExpose({
</div>
<q-card
v-show="poiPlaceName"
bordered
flat
class="col-12 bg-grey-2 shadow-0"
@ -314,8 +747,8 @@ defineExpose({
>
<div v-if="$q.screen.gt.xs">
<div
id="mapViewDisplay"
ref="mapElement"
:id="mapContainerId"
ref="desktopMapContainerRef"
style="height: 35vh; pointer-events: none"
></div>
@ -339,6 +772,7 @@ defineExpose({
<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
@ -355,16 +789,20 @@ defineExpose({
name="mdi-information-outline"
size="xs"
class="q-mr-xs cursor-pointer"
@click.stop="$q.dialog({ message: textTooltip, ok: 'ปิด' })"
@click.stop="$q.dialog({ message: textTooltip, ok: 'ตกลง' })"
/>
</q-item-label>
<q-item-label class="text-weight-medium text-grey-9">
{{ poiPlaceName }}
{{ poiPlaceName || '-' }}
</q-item-label>
</q-item-section>
</template>
<div id="mapViewDisplay" style="height: 20vh"></div>
<div
:id="mapContainerId"
ref="mobileMapContainerRef"
style="height: 20vh"
></div>
</q-expansion-item>
</q-card>
</q-card>

View file

@ -1,10 +1,8 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { nextTick, onBeforeUnmount, onMounted, shallowRef, ref } from 'vue'
import { loadModules } from 'esri-loader'
import axios from 'axios'
import type { LocationObject } from '@/interface/index/Main'
const emit = defineEmits(['update:location'])
function updateLocation(latitude: number, longitude: number, namePOI: string) {
@ -20,8 +18,67 @@ const apiKey = ref<string>(
// 'AAPK4f700a4324d04e9f8a1a134e0771ac45FXWawdCl-OotFfr52gz9XKxTDJTpDzw_YYcwbmKDDyAJswf14FoPyw0qBkN64DvP'
)
const zoomMap = ref<number>(18)
const mapViewRef = shallowRef<any>(null)
const mapContainerRef = ref<HTMLElement | null>(null)
const mapRenderKey = ref<number>(0)
let resizeTimer: ReturnType<typeof setTimeout> | null = null
let resizeRetryCount = 0
const MAX_RESIZE_RETRY = 8
const isRecreatingMap = ref<boolean>(false)
function scheduleMapResize() {
if (resizeTimer) {
clearTimeout(resizeTimer)
}
// Delay a little to wait for viewport + CSS layout to settle after rotation.
resizeTimer = setTimeout(async () => {
await nextTick()
if (!mapViewRef.value || !mapContainerRef.value) {
return
}
const container = mapContainerRef.value
const mapView = mapViewRef.value
if (container.clientWidth === 0 || container.clientHeight === 0) {
if (resizeRetryCount < MAX_RESIZE_RETRY) {
resizeRetryCount += 1
scheduleMapResize()
}
return
}
resizeRetryCount = 0
// Force canvas to bind to the current element after repeated rotations.
if (mapView.container !== container) {
mapView.container = null
await nextTick()
mapView.container = container
}
mapView.resize()
mapView
.goTo(
{
center: mapView.center,
zoom: mapView.zoom,
},
{ animate: false }
)
.catch(() => {
// Ignore interruption errors from rapid orientation changes.
})
}, 300)
}
async function initializeMap() {
if (mapViewRef.value || !mapContainerRef.value) {
return
}
try {
// Load modules of ArcGIS
loadModules([
@ -67,7 +124,7 @@ async function initializeMap() {
// MapView
const mapView = new MapView({
container: 'mapViewDisplay', // div
container: mapContainerRef.value, // div
map: map,
center: {
latitude: latitude,
@ -79,6 +136,7 @@ async function initializeMap() {
components: [], // Empty array to remove all default UI components
},
})
mapViewRef.value = mapView
//
const markerSymbol = new PictureMarkerSymbol({
@ -98,9 +156,42 @@ async function initializeMap() {
position: 'top-right', // Search widget
})
// token .
// 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,
// })
// updateLocation(latitude, longitude, poiPlaceName.value)
// })
// .catch(async () => {
await axios
.get(
'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/',
'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/',
{
params: {
f: 'json', // Format JSON response
@ -111,52 +202,20 @@ async function initializeMap() {
x: longitude,
y: latitude,
},
token: apiKey.value,
},
}
)
.then((response) => {
console.log('poi', response.data.location)
// 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,
})
updateLocation(latitude, longitude, poiPlaceName.value)
})
.catch(async () => {
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) => {
// console.log('poi', response.data.location)
poiPlaceName.value = response.data.address
? response.data.address.PlaceName === ''
? response.data.address.ShortLabel
: response.data.address.PlaceName
: 'ไม่พบข้อมูล'
updateLocation(longitude, latitude, poiPlaceName.value)
})
updateLocation(longitude, latitude, poiPlaceName.value)
})
// })
let searchMarker: any = null
@ -186,9 +245,41 @@ async function initializeMap() {
symbol: markerSymbol,
})
if (searchMarker) {
// 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,
// })
// updateLocation(latitude, longitude, poiPlaceName.value)
// })
// .catch(async () => {
await axios
.get(
'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/',
'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/',
{
params: {
f: 'json', // Format JSON response
@ -199,52 +290,20 @@ async function initializeMap() {
x: longitude,
y: latitude,
},
token: apiKey.value,
},
}
)
.then((response) => {
console.log('poi', response.data.location)
// 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,
})
updateLocation(latitude, longitude, poiPlaceName.value)
})
.catch(async () => {
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) => {
// console.log('poi', response.data.location)
poiPlaceName.value = response.data.address
? response.data.address.PlaceName === ''
? response.data.address.ShortLabel
: response.data.address.PlaceName
: 'ไม่พบข้อมูล'
updateLocation(longitude, latitude, poiPlaceName.value)
})
updateLocation(longitude, latitude, poiPlaceName.value)
})
// })
}
mapView.graphics.add(searchMarker)
@ -275,9 +334,41 @@ async function initializeMap() {
} else {
searchMarker.geometry = point //
if (searchMarker) {
// 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: lon,
// y: lat,
// },
// 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,
// })
// updateLocation(latitude, longitude, poiPlaceName.value)
// })
// .catch(async () => {
await axios
.get(
'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/',
'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/',
{
params: {
f: 'json', // Format JSON response
@ -288,52 +379,20 @@ async function initializeMap() {
x: lon,
y: lat,
},
token: apiKey.value,
},
}
)
.then((response) => {
console.log('poi', response.data.location)
// 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,
})
updateLocation(latitude, longitude, poiPlaceName.value)
})
.catch(async () => {
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: lon,
y: lat,
},
},
}
)
.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
: 'ไม่พบข้อมูล'
updateLocation(lat, lon, poiPlaceName.value)
})
updateLocation(lat, lon, poiPlaceName.value)
})
// })
}
}
@ -350,14 +409,64 @@ async function initializeMap() {
}
}
async function recreateMapOnOrientationChange() {
if (isRecreatingMap.value) {
return
}
isRecreatingMap.value = true
try {
if (resizeTimer) {
clearTimeout(resizeTimer)
resizeTimer = null
}
if (mapViewRef.value) {
mapViewRef.value.destroy()
mapViewRef.value = null
}
mapRenderKey.value += 1
await nextTick()
await initializeMap()
scheduleMapResize()
} finally {
isRecreatingMap.value = false
}
}
onMounted(async () => {
await initializeMap()
window.addEventListener('resize', scheduleMapResize)
window.addEventListener('orientationchange', recreateMapOnOrientationChange)
window.addEventListener('pageshow', scheduleMapResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', scheduleMapResize)
window.removeEventListener(
'orientationchange',
recreateMapOnOrientationChange
)
window.removeEventListener('pageshow', scheduleMapResize)
if (resizeTimer) {
clearTimeout(resizeTimer)
resizeTimer = null
}
if (mapViewRef.value) {
mapViewRef.value.destroy()
mapViewRef.value = null
}
})
</script>
<template>
<q-card bordered flat class="col-12 bg-grey-2 shadow-0">
<div id="mapViewDisplay" style="height: 35vh"></div>
<div :key="mapRenderKey" ref="mapContainerRef" style="height: 35vh"></div>
<div
:class="

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, reactive, onMounted, watch, onBeforeUnmount } from 'vue'
import { ref, reactive, onMounted, watch, onBeforeUnmount, computed } from 'vue'
import { useQuasar } from 'quasar'
import { format } from 'date-fns'
import Camera from 'simple-vue-camera'
@ -466,7 +466,12 @@ async function validateForm() {
}
}
const mapRef = ref<InstanceType<typeof MapCheck> | null>(null)
const mapRefDesktop = ref<InstanceType<typeof MapCheck> | null>(null)
const mapRefMobile = ref<InstanceType<typeof MapCheck> | null>(null)
// Computed ref that returns the correct map ref based on screen size
const mapRef = computed(() => ($q.screen.gt.xs ? mapRefDesktop.value : mapRefMobile.value))
const timeChickin = ref<string>('') //,
/** function ยืนยันการลงเวลาเข้า - ออก*/
@ -777,13 +782,13 @@ watch(
<div class="col-xs-12 col-sm-8 gt-xs">
<div class="col-12">
<MapCheck
v-if="$q.screen.gt.xs"
ref="mapRef"
ref="mapRefDesktop"
:initialPOI="formLocation.POI"
@update:location="updateLocation"
/>
</div>
</div>
<div class="col-xs-12 col-sm-4">
<div class="col-xs-12 col-sm-4 gt-xs">
<q-card
:class="
$q.screen.xs ? 'card-container-xs' : 'card-container'
@ -893,6 +898,81 @@ watch(
<!-- กรอกขอม หนามอถ -->
<div class="col-12 row q-col-gutter-y-md" v-if="$q.screen.xs">
<!-- กลอง หนามอถ -->
<div class="col-12">
<q-card
:class="
$q.screen.xs ? 'card-container-xs' : 'card-container'
"
>
<div
v-if="!cameraIsOn && img == null"
class="preview-placeholder"
@click="() => !isDisabledCheckTime && openCamera()"
>
<div class="text-center">
<q-icon
name="mdi-camera"
color="blue-grey-3"
size="100px"
class="center-icon"
/>
</div>
</div>
<div class="col-12 row items-center">
<!-- แสดงกลองตอนกดถายภาพ -->
<Camera
:resolution="{ width: photoWidth, height: photoHeight }"
ref="camera"
:autoplay="false"
:style="!img ? 'display: block' : 'display: none'"
/>
<!-- แสดงรปเมอกด capture -->
<div v-if="img" class="image-container">
<q-img :src="img" class="image-element"></q-img>
</div>
<div v-if="cameraIsOn">
<div
class="absolute-bottom text-subtitle2 text-center q-py-sm"
style="background: #00000021"
>
<q-btn
v-if="availableCameras.length > 1 && img == null"
round
icon="flip_camera_ios"
size="16px"
style="background: #424242; color: white"
@click="switchCamera"
unelevated
class="q-mr-xs"
/>
<q-btn
round
v-if="img == null"
icon="photo_camera"
size="18px"
style="background: #263238; color: white"
@click="capturePhoto"
unelevated
/>
<q-btn
v-else
round
icon="refresh"
size="18px"
style="background: #263238; color: white"
@click="refreshPhoto"
unelevated
/>
</div>
</div>
</div>
</q-card>
</div>
<div class="col-12" v-if="!isDisabledCheckTime">
<q-card
flat
@ -965,8 +1045,13 @@ watch(
</q-card>
</div>
<div class="col-12" v-if="$q.screen.xs">
<MapCheck ref="mapRef" @update:location="updateLocation" />
<!-- Map หนามอถ -->
<div class="col-12">
<MapCheck
ref="mapRefMobile"
:initialPOI="formLocation.POI"
@update:location="updateLocation"
/>
</div>
</div>
<!-- กรอกขอม หนามอถ -->