hrms-checkin/src/components/AscGISMap.vue

378 lines
13 KiB
Vue
Raw Normal View History

2023-12-08 13:15:13 +07:00
<script setup lang="ts">
2026-04-27 19:21:23 +07:00
import { ref, shallowRef, onBeforeUnmount } from 'vue'
2023-12-08 13:15:13 +07:00
import { loadModules } from 'esri-loader'
import axios from 'axios'
2025-06-13 11:24:09 +07:00
import { useCounterMixin } from '@/stores/mixin'
import { useQuasar } from 'quasar'
2026-01-19 14:47:32 +07:00
import { usePrivacyStore } from '@/stores/privacy'
2024-09-02 17:37:08 +07:00
2025-06-13 11:24:09 +07:00
const mixin = useCounterMixin()
const { messageError } = mixin
2026-01-19 14:47:32 +07:00
const privacyStore = usePrivacyStore()
2023-12-08 13:15:13 +07:00
2026-03-30 10:47:36 +07:00
const emit = defineEmits(['update:location'])
2025-06-13 11:24:09 +07:00
const $q = useQuasar()
2023-12-08 14:24:53 +07:00
2026-04-27 19:21:23 +07:00
// Throttle mechanism to prevent excessive location updates
const LOCATION_UPDATE_THROTTLE_MS = 500
let lastLocationUpdate = 0
2024-09-02 17:37:08 +07:00
function updateLocation(latitude: number, longitude: number, namePOI: string) {
2026-04-27 19:21:23 +07:00
const now = Date.now()
// Skip update if called too frequently (within throttle period)
if (now - lastLocationUpdate < LOCATION_UPDATE_THROTTLE_MS) {
return
}
lastLocationUpdate = now
2023-12-08 14:24:53 +07:00
// ส่ง event ไปยัง parent component เพื่ออัพเดทค่า props
emit('update:location', latitude, longitude, namePOI)
}
2024-09-02 17:37:08 +07:00
const poiPlaceName = ref<string>('') // ชื่อพื้นที่ใกล้เคียง
2026-04-27 19:21:23 +07:00
const mapView = shallowRef<any>(null) // Store mapView reference for cleanup (use shallowRef to avoid reactivity issues)
2023-12-08 13:15:13 +07:00
2026-04-27 19:21:23 +07:00
// ArcGIS API key from environment variable
2023-12-08 13:15:13 +07:00
const apiKey = ref<string>(
2026-04-27 20:18:09 +07:00
'YLATgWuywoeRLHn6KImj5rg7UaP8bJoR9jiTldoCVBHlqFIebwMSA5wIXEmcYhwXwMHkmNISEYtUz3x0oiGIIx0bIXXnUwi0OzupoOEtDrQIsRPVtor7gaPpXEmH8TrNaMT3snf6zO_yujHLGzborg-L9aeAjTJn4ndL6f8qFmRzYcX93E2vyA-7XCufLYTRsdTE5Aq-9hnx1q9PmYVMqhAZpL7dWqn3JgO33fRXetk.'
2023-12-08 13:15:13 +07:00
)
2024-10-22 17:00:23 +07:00
const zoomMap = ref<number>(18)
const textTooltip = ref<string>(
2026-04-27 19:21:23 +07:00
'พื้นที่ใกล้เคียงคือ สถานที่สำคัญรอบตัวคุณ (ไม่ใช่ตำแหน่งปัจจุบัน)'
)
2023-12-08 13:15:13 +07:00
2026-03-30 10:47:36 +07:00
async function initializeMap() {
2023-12-08 13:15:13 +07:00
try {
// Load modules of ArcGIS
2024-10-22 08:43:30 +07:00
loadModules([
2023-12-08 13:15:13 +07:00
'esri/config',
'esri/Map',
'esri/views/MapView',
'esri/geometry/Point',
'esri/Graphic',
2024-10-22 17:00:23 +07:00
'esri/layers/TileLayer',
]).then(async ([esriConfig, Map, MapView, Point, Graphic, TileLayer]) => {
2026-03-30 10:47:36 +07:00
// 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`,
// })
2024-10-22 17:00:23 +07:00
const map = new Map({
basemap: 'streets',
2026-03-30 10:47:36 +07:00
// basemap: 'arcgis-topographic',
// layers: [hillshadeLayer],
2024-10-22 17:00:23 +07:00
})
2026-03-30 10:47:36 +07:00
navigator.geolocation.getCurrentPosition(async (position) => {
const { latitude, longitude } = position.coords
2026-04-27 19:21:23 +07:00
mapView.value = new MapView({
2026-03-30 10:47:36 +07:00
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
},
})
2024-10-22 08:43:30 +07:00
2026-03-30 10:47:36 +07:00
// ตำแหน่งของผู้ใช้
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,
})
2026-04-27 19:21:23 +07:00
mapView.value.graphics.add(userGraphic)
2026-03-30 10:47:36 +07:00
// 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,
2024-10-22 17:00:23 +07:00
},
2026-03-30 10:47:36 +07:00
}
)
.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,
})
2026-04-27 19:21:23 +07:00
mapView.value.graphics.add(poiGraphic)
2026-03-30 10:47:36 +07:00
// อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI
2026-04-27 19:21:23 +07:00
mapView.value.goTo({
2026-03-30 10:47:36 +07:00
target: [userPoint, poiPoint],
zoom: zoomMap.value,
})
2024-11-07 13:49:42 +07:00
2026-03-30 10:47:36 +07:00
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,
},
2026-03-11 15:29:43 +07:00
},
2026-03-30 10:47:36 +07:00
}
)
.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,
})
2026-04-27 19:21:23 +07:00
mapView.value.graphics.add(poiGraphic)
2026-03-30 10:47:36 +07:00
// อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI
2026-04-27 19:21:23 +07:00
mapView.value.goTo({
2026-03-30 10:47:36 +07:00
target: [userPoint, poiPoint],
zoom: zoomMap.value,
})
updateLocation(latitude, longitude, poiPlaceName.value)
2024-11-07 13:49:42 +07:00
})
2026-03-30 10:47:36 +07:00
.catch((error) => {
// console.error('Error fetching points of interest:', error)
2026-03-11 15:29:43 +07:00
})
2026-03-30 10:47:36 +07:00
})
})
2024-10-22 17:00:23 +07:00
})
2023-12-08 13:15:13 +07:00
} catch (error) {
console.error('Error loading the map', error)
}
}
2026-03-06 23:19:31 +07:00
const locationGranted = ref<boolean>(false)
2025-06-13 11:24:09 +07:00
// Function to request location permission
const requestLocationPermission = () => {
2026-01-19 14:47:32 +07:00
// เช็คสิทธิ์ privacy ก่อนเข้าถึงแผนที่
if (!privacyStore.isAccepted) {
$q.notify({
type: 'warning',
message: 'กรุณายอมรับนโยบายคุ้มครองข้อมูลส่วนบุคคลก่อนใช้งานแผนที่',
position: 'top',
})
return
}
2025-06-13 11:24:09 +07:00
if (!navigator.geolocation) {
messageError(
$q,
'',
'ไม่สามารถระบุตำแหน่งปัจจุบันได้ เบราว์เซอร์ของคุณไม่รองรับ Geolocation'
)
return
}
navigator.geolocation.getCurrentPosition(
2026-02-06 17:06:55 +07:00
async (position) => {
2026-03-30 10:47:36 +07:00
// Permission granted
2026-03-11 17:44:39 +07:00
locationGranted.value = true
2025-06-13 11:24:09 +07:00
2026-03-30 10:47:36 +07:00
const { latitude, longitude } = position.coords
// console.log('Current position:', latitude, longitude)
if (!latitude || !longitude) {
messageError($q, '', 'ไม่สามารถระบุตำแหน่งปัจจุบันได้')
return
}
2025-06-13 11:24:09 +07:00
// Center map on user's location if map is initialized
2026-02-06 17:06:55 +07:00
if (privacyStore.isAccepted) {
2026-03-30 10:47:36 +07:00
await initializeMap()
2026-02-06 17:06:55 +07:00
}
2025-06-13 11:24:09 +07:00
},
(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
}
},
2026-03-30 10:47:36 +07:00
{ enableHighAccuracy: true }
2025-06-13 11:24:09 +07:00
)
}
2026-04-27 19:21:23 +07:00
// Cleanup map resources when component unmounts
onBeforeUnmount(() => {
if (mapView.value) {
mapView.value.destroy()
mapView.value = null
}
})
2025-06-13 11:24:09 +07:00
defineExpose({
requestLocationPermission,
})
2023-12-08 13:15:13 +07:00
</script>
<template>
<!-- Loading skeleton -->
2026-03-30 10:47:36 +07:00
<div v-if="!poiPlaceName" class="col-12">
2025-03-17 17:10:24 +07:00
<q-skeleton
:height="$q.screen.gt.xs ? '35vh' : '45px'"
width="100%"
:style="$q.screen.gt.xs ? ';' : 'border-radius: 20px'"
class="bg-grey-4"
/>
2023-12-08 13:15:13 +07:00
</div>
2025-03-17 17:10:24 +07:00
<q-card
2026-03-30 10:47:36 +07:00
v-show="poiPlaceName"
2025-03-17 17:10:24 +07:00
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">
2025-06-13 11:24:09 +07:00
<div
id="mapViewDisplay"
ref="mapElement"
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'
"
>
นทใกลเคยง
2026-04-21 15:25:59 +07:00
<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>
2026-04-27 19:21:23 +07:00
<span class="q-px-sm">:</span>
{{ poiPlaceName }}
</div>
2023-12-08 13:15:13 +07:00
</div>
2025-03-17 17:10:24 +07:00
<q-card v-else style="border-radius: 20px">
<q-expansion-item
2026-04-21 15:25:59 +07:00
class="shadow-1 overflow-hidden bg-grey-4 text-left q-pa-xs"
style="border-radius: 20px"
dense
2025-03-17 17:10:24 +07:00
default-opened
>
<template v-slot:header>
2025-03-17 17:10:24 +07:00
<q-item-section avatar class="q-pr-none expanAS">
2026-04-21 15:25:59 +07:00
<q-icon name="mdi-map-marker" color="primary" />
2025-03-17 17:10:24 +07:00
</q-item-section>
<q-item-section>
2026-04-21 15:25:59 +07:00
<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: 'ปิด' })"
2026-04-21 15:25:59 +07:00
/>
</q-item-label>
<q-item-label class="text-weight-medium text-grey-9">
{{ poiPlaceName }}
</q-item-label>
2025-03-17 17:10:24 +07:00
</q-item-section>
</template>
2025-03-17 17:10:24 +07:00
<div id="mapViewDisplay" style="height: 20vh"></div>
</q-expansion-item>
</q-card>
2023-12-08 13:15:13 +07:00
</q-card>
</template>
2025-03-17 17:10:24 +07:00
<style>
.expanAS.q-item__section--avatar {
min-width: 40px !important;
}
</style>