fix track mock location

This commit is contained in:
Warunee Tamkoo 2026-03-11 15:29:43 +07:00
parent 7fdece0a28
commit 88352279a9
2 changed files with 206 additions and 158 deletions

View file

@ -32,7 +32,7 @@ const apiKey = ref<string>(
) )
const zoomMap = ref<number>(18) const zoomMap = ref<number>(18)
async function initializeMap() { async function initializeMap(position: GeolocationPosition) {
try { try {
// Load modules of ArcGIS // Load modules of ArcGIS
loadModules([ loadModules([
@ -43,159 +43,146 @@ async function initializeMap() {
'esri/Graphic', 'esri/Graphic',
'esri/layers/TileLayer', 'esri/layers/TileLayer',
]).then(async ([esriConfig, Map, MapView, Point, Graphic, TileLayer]) => { ]).then(async ([esriConfig, Map, MapView, Point, Graphic, TileLayer]) => {
// 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({ const map = new Map({
basemap: 'streets', basemap: 'streets',
// basemap: 'arcgis-topographic',
// layers: [hillshadeLayer],
}) })
navigator.geolocation.getCurrentPosition(async (position) => { const { latitude, longitude } = position.coords
const { latitude, longitude } = position.coords
const mapView = new MapView({ const mapView = new MapView({
container: 'mapViewDisplay', container: 'mapViewDisplay',
map: map, map: map,
center: { center: {
latitude: latitude, latitude: latitude,
longitude: longitude, longitude: longitude,
}, // Set the initial map center current position }, // Set the initial map center current position
zoom: zoomMap.value, zoom: zoomMap.value,
constraints: { constraints: {
snapToZoom: false, // Disables snapping to the zoom level snapToZoom: false, // Disables snapping to the zoom level
minZoom: zoomMap.value, // Set minimum zoom level minZoom: zoomMap.value, // Set minimum zoom level
maxZoom: zoomMap.value, // Set maximum zoom level (same as minZoom for fixed zoom) maxZoom: zoomMap.value, // Set maximum zoom level (same as minZoom for fixed zoom)
}, },
ui: { ui: {
components: [], // Empty array to remove all default UI components components: [], // Empty array to remove all default UI components
}, },
}) })
// //
const userPoint = new Point({ longitude, latitude }) const userPoint = new Point({ longitude, latitude })
const userSymbol = { const userSymbol = {
type: 'picture-marker', type: 'picture-marker',
url: 'http://maps.google.com/mapfiles/ms/icons/red.png', url: 'http://maps.google.com/mapfiles/ms/icons/red.png',
width: '32px', width: '32px',
height: '32px', height: '32px',
} }
const userGraphic = new Graphic({ const userGraphic = new Graphic({
geometry: userPoint, geometry: userPoint,
symbol: userSymbol, symbol: userSymbol,
}) })
mapView.graphics.add(userGraphic) mapView.graphics.add(userGraphic)
// Get POI place server . // Get POI place server .
await axios await axios
.get( .get(
'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/', 'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/',
{ {
params: { params: {
f: 'json', // Format JSON response f: 'json', // Format JSON response
distance: 2000, distance: 2000,
category: 'POI', category: 'POI',
location: { location: {
spatialReference: { wkid: 4326 }, spatialReference: { wkid: 4326 },
x: longitude, x: longitude,
y: latitude, y: latitude,
},
token: apiKey.value,
}, },
} token: apiKey.value,
) },
.then((response) => { }
// console.log('poi', response.data.location) )
poiPlaceName.value = response.data.address .then((response) => {
? response.data.address.PlaceName === '' // console.log('poi', response.data.location)
? response.data.address.ShortLabel poiPlaceName.value = response.data.address
: response.data.address.PlaceName ? response.data.address.PlaceName === ''
: 'ไม่พบข้อมูล' ? response.data.address.ShortLabel
const poiPoint = new Point({ : response.data.address.PlaceName
longitude: response.data.location.x, : 'ไม่พบข้อมูล'
latitude: response.data.location.y, const poiPoint = new Point({
}) longitude: response.data.location.x,
const poiSymbol = { latitude: response.data.location.y,
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.graphics.add(poiGraphic)
// POI
mapView.goTo({
target: [userPoint, poiPoint],
zoom: zoomMap.value,
})
updateLocation(latitude, longitude, poiPlaceName.value)
}) })
.catch(async (error) => { const poiSymbol = {
// console.error('Error fetching points of interest:', error) type: 'picture-marker',
// Get POI place server arcgis token url: 'http://maps.google.com/mapfiles/ms/icons/blue.png',
await axios width: '32px',
.get( height: '32px',
'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/', }
{ const poiGraphic = new Graphic({
params: { geometry: poiPoint,
f: 'json', // Format JSON response symbol: poiSymbol,
distance: 2000, })
category: 'POI', mapView.graphics.add(poiGraphic)
location: { // POI
spatialReference: { wkid: 4326 }, mapView.goTo({
x: longitude, target: [userPoint, poiPoint],
y: latitude, 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) .then((response) => {
poiPlaceName.value = response.data.address // console.log('poi', response.data.location)
? response.data.address.PlaceName === '' poiPlaceName.value = response.data.address
? response.data.address.ShortLabel ? response.data.address.PlaceName === ''
: response.data.address.PlaceName ? response.data.address.ShortLabel
: 'ไม่พบข้อมูล' : response.data.address.PlaceName
const poiPoint = new Point({ : 'ไม่พบข้อมูล'
longitude: response.data.location.x, const poiPoint = new Point({
latitude: response.data.location.y, longitude: response.data.location.x,
}) latitude: response.data.location.y,
const poiSymbol = { })
type: 'picture-marker', const poiSymbol = {
url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', type: 'picture-marker',
width: '32px', url: 'http://maps.google.com/mapfiles/ms/icons/blue.png',
height: '32px', width: '32px',
} height: '32px',
const poiGraphic = new Graphic({ }
geometry: poiPoint, const poiGraphic = new Graphic({
symbol: poiSymbol, geometry: poiPoint,
}) symbol: poiSymbol,
mapView.graphics.add(poiGraphic) })
// POI mapView.graphics.add(poiGraphic)
mapView.goTo({ // POI
target: [userPoint, poiPoint], mapView.goTo({
zoom: zoomMap.value, target: [userPoint, poiPoint],
}) zoom: zoomMap.value,
})
updateLocation(latitude, longitude, poiPlaceName.value) updateLocation(latitude, longitude, poiPlaceName.value)
}) })
.catch((error) => { .catch((error) => {
// console.error('Error fetching points of interest:', error) // console.error('Error fetching points of interest:', error)
}) })
}) })
})
}) })
} catch (error) { } catch (error) {
console.error('Error loading the map', error) console.error('Error loading the map', error)
@ -236,7 +223,7 @@ const requestLocationPermission = () => {
} }
// Check for critical errors (invalid coordinates) that prevent showing location // Check for critical errors (invalid coordinates) that prevent showing location
const hasCriticalErrors = validationResult.errors.some(error => const hasCriticalErrors = validationResult.errors.some((error) =>
error.includes('พิกัดตำแหน่งไม่ถูกต้อง') error.includes('พิกัดตำแหน่งไม่ถูกต้อง')
) )
@ -261,7 +248,7 @@ const requestLocationPermission = () => {
// Center map on user's location if map is initialized // Center map on user's location if map is initialized
if (privacyStore.isAccepted) { if (privacyStore.isAccepted) {
await initializeMap() await initializeMap(position)
} }
}, },
(error) => { (error) => {

View file

@ -23,6 +23,7 @@ export const VALIDATION_CONFIG = {
MAX_SPEED_MS: 100, // 100 m/s (~360 km/h) - maximum plausible movement speed MAX_SPEED_MS: 100, // 100 m/s (~360 km/h) - maximum plausible movement speed
POSITION_HISTORY_SIZE: 5, // number of positions to keep for pattern detection POSITION_HISTORY_SIZE: 5, // number of positions to keep for pattern detection
MOCK_INDICATOR_THRESHOLD: 3, // threshold for mock detection (indicators >= 3 = mock) MOCK_INDICATOR_THRESHOLD: 3, // threshold for mock detection (indicators >= 3 = mock)
SUSPICIOUS_ACCURACY_MAX: 5, // accuracy ≤ 5m AND integer = suspicious (real GPS never rounds to whole number)
} as const } as const
export function useLocationValidation() { export function useLocationValidation() {
@ -31,17 +32,26 @@ export function useLocationValidation() {
// Thai error messages - exported for i18n consistency // Thai error messages - exported for i18n consistency
const errorMessages = { const errorMessages = {
MOCK_DETECTED: 'ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง กรุณาปิดแอปจำลองตำแหน่งและลองใหม่', MOCK_DETECTED:
'ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง กรุณาปิดแอปจำลองตำแหน่งและลองใหม่',
INVALID_COORDINATES: 'พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่', INVALID_COORDINATES: 'พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่',
STALE_TIMESTAMP: 'ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่', STALE_TIMESTAMP: 'ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่',
POOR_ACCURACY: 'ความแม่นยำตำแหน่งต่ำเกินไป กรุณาตรวจสอบการรับสัญญาณ GPS', POOR_ACCURACY: 'ความแม่นยำตำแหน่งต่ำเกินไป กรุณาตรวจสอบการรับสัญญาณ GPS',
IMPOSSIBLE_SPEED: 'ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง', IMPOSSIBLE_SPEED:
'ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง',
SUSPICIOUS_ACCURACY: 'ตรวจพบค่าความแม่นยำที่ผิดปกติ อาจเป็นการจำลองตำแหน่ง',
DUPLICATE_POSITION: 'ตรวจพบพิกัดตำแหน่งซ้ำกัน อาจเป็นการจำลองตำแหน่ง',
} }
const previousPositions = ref<PositionSnapshot[]>([]) const previousPositions = ref<PositionSnapshot[]>([])
// คำนวณระยะห่างระหว่าง 2 จุด (Haversine formula) // คำนวณระยะห่างระหว่าง 2 จุด (Haversine formula)
const haversineDistance = (lat1: number, lon1: number, lat2: number, lon2: number): number => { const haversineDistance = (
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number => {
const R = 6371e3 // Earth's radius in meters const R = 6371e3 // Earth's radius in meters
const φ1 = (lat1 * Math.PI) / 180 const φ1 = (lat1 * Math.PI) / 180
const φ2 = (lat2 * Math.PI) / 180 const φ2 = (lat2 * Math.PI) / 180
@ -59,9 +69,12 @@ export function useLocationValidation() {
// ตรวจสอบพิกัดถูกต้อง // ตรวจสอบพิกัดถูกต้อง
const validateCoordinates = (lat: number, lon: number): boolean => { const validateCoordinates = (lat: number, lon: number): boolean => {
return ( return (
lat >= -90 && lat <= 90 && lat >= -90 &&
lon >= -180 && lon <= 180 && lat <= 90 &&
!isNaN(lat) && !isNaN(lon) && lon >= -180 &&
lon <= 180 &&
!isNaN(lat) &&
!isNaN(lon) &&
!(lat === 0 && lon === 0) // Mock มักใช้ 0,0 !(lat === 0 && lon === 0) // Mock มักใช้ 0,0
) )
} }
@ -80,20 +93,49 @@ export function useLocationValidation() {
} }
// คำนวณความเร็ว // คำนวณความเร็ว
const calculateSpeed = (pos1: PositionSnapshot, pos2: PositionSnapshot): number => { const calculateSpeed = (
const distance = haversineDistance(pos1.latitude, pos1.longitude, pos2.latitude, pos2.longitude) pos1: PositionSnapshot,
pos2: PositionSnapshot
): number => {
const distance = haversineDistance(
pos1.latitude,
pos1.longitude,
pos2.latitude,
pos2.longitude
)
const timeDiff = Math.abs(pos2.timestamp - pos1.timestamp) / 1000 // seconds const timeDiff = Math.abs(pos2.timestamp - pos1.timestamp) / 1000 // seconds
return timeDiff > 0 ? distance / timeDiff : 0 return timeDiff > 0 ? distance / timeDiff : 0
} }
// ตรวจสอบความเร็วปกติ // ตรวจสอบความเร็วปกติ
const validateSpeed = (current: PositionSnapshot, previous: PositionSnapshot): boolean => { const validateSpeed = (
current: PositionSnapshot,
previous: PositionSnapshot
): boolean => {
const speed = calculateSpeed(previous, current) const speed = calculateSpeed(previous, current)
return speed <= VALIDATION_CONFIG.MAX_SPEED_MS return speed <= VALIDATION_CONFIG.MAX_SPEED_MS
} }
// ตรวจสอบค่าความแม่นยำที่ผิดปกติ (mock apps มักรายงานค่าที่เป็นเลขจำนวนเต็มสวยงาม เช่น 5.0, 3.0)
const hasSuspiciousPerfectAccuracy = (accuracy: number | null): boolean => {
if (accuracy === null) return false
return (
Number.isInteger(accuracy) &&
accuracy <= VALIDATION_CONFIG.SUSPICIOUS_ACCURACY_MAX
)
}
// ตรวจสอบพิกัดซ้ำกันทุกค่า (real GPS มีการสั่นเล็กน้อย mock app มักคืนพิกัดเดิมซ้ำ)
const hasDuplicateCoordinates = (lat: number, lon: number): boolean => {
return previousPositions.value.some(
(p) => p.latitude === lat && p.longitude === lon
)
}
// Main validation function // Main validation function
const validateLocation = (position: GeolocationPosition): LocationValidationResult => { const validateLocation = (
position: GeolocationPosition
): LocationValidationResult => {
const warnings: string[] = [] const warnings: string[] = []
const errors: string[] = [] const errors: string[] = []
let mockIndicators = 0 let mockIndicators = 0
@ -121,7 +163,8 @@ export function useLocationValidation() {
// 4. Compare with previous positions // 4. Compare with previous positions
if (previousPositions.value.length > 0) { if (previousPositions.value.length > 0) {
const previous = previousPositions.value[previousPositions.value.length - 1] const previous =
previousPositions.value[previousPositions.value.length - 1]
if (!validateSpeed({ latitude, longitude, timestamp }, previous)) { if (!validateSpeed({ latitude, longitude, timestamp }, previous)) {
errors.push(errorMessages.IMPOSSIBLE_SPEED) errors.push(errorMessages.IMPOSSIBLE_SPEED)
@ -129,14 +172,32 @@ export function useLocationValidation() {
} }
} }
// 5. Suspiciously perfect accuracy — real GPS never rounds to a whole-number ≤ 5m
if (hasSuspiciousPerfectAccuracy(accuracy)) {
warnings.push(errorMessages.SUSPICIOUS_ACCURACY)
mockIndicators += 2
}
// 6. Exact duplicate coordinates — real GPS always drifts slightly between readings
if (
previousPositions.value.length > 0 &&
hasDuplicateCoordinates(latitude, longitude)
) {
warnings.push(errorMessages.DUPLICATE_POSITION)
mockIndicators += 2
}
// Store current position // Store current position
previousPositions.value.push({ latitude, longitude, timestamp }) previousPositions.value.push({ latitude, longitude, timestamp })
if (previousPositions.value.length > VALIDATION_CONFIG.POSITION_HISTORY_SIZE) { if (
previousPositions.value.length > VALIDATION_CONFIG.POSITION_HISTORY_SIZE
) {
previousPositions.value.shift() previousPositions.value.shift()
} }
// Determine result // Determine result
const isMockDetected = mockIndicators >= VALIDATION_CONFIG.MOCK_INDICATOR_THRESHOLD const isMockDetected =
mockIndicators >= VALIDATION_CONFIG.MOCK_INDICATOR_THRESHOLD
const isValid = errors.length === 0 const isValid = errors.length === 0
let confidence: 'low' | 'medium' | 'high' = 'low' let confidence: 'low' | 'medium' | 'high' = 'low'
@ -154,7 +215,7 @@ export function useLocationValidation() {
const showMockWarning = (result: LocationValidationResult) => { const showMockWarning = (result: LocationValidationResult) => {
if (!result.isMockDetected) return if (!result.isMockDetected) return
messageError($q, null, errorMessages.MOCK_DETECTED) messageError($q, '', errorMessages.MOCK_DETECTED)
} }
const resetValidation = () => { const resetValidation = () => {