142 lines
6.2 KiB
Java
142 lines
6.2 KiB
Java
package com.walkguide.service;
|
|
|
|
import com.walkguide.dto.request.LocationUpdateRequest;
|
|
import com.walkguide.dto.response.LocationResponse;
|
|
import com.walkguide.entity.LocationHistory;
|
|
import com.walkguide.entity.User;
|
|
import com.walkguide.enums.ActivityLogType;
|
|
import com.walkguide.enums.PairingStatus;
|
|
import com.walkguide.exception.PairingException;
|
|
import com.walkguide.exception.ResourceNotFoundException;
|
|
import com.walkguide.repository.*;
|
|
import com.walkguide.websocket.LocationBroadcaster;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.data.domain.Page;
|
|
import org.springframework.data.domain.Pageable;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
public class LocationService {
|
|
|
|
private final LocationHistoryRepository locationHistoryRepository;
|
|
private final GeofenceConfigRepository geofenceConfigRepository;
|
|
private final PairingRelationRepository pairingRelationRepository;
|
|
private final UserRepository userRepository;
|
|
private final ActivityLogService activityLogService;
|
|
private final FcmService fcmService;
|
|
|
|
// ✅ BARU: WebSocket broadcaster untuk real-time location ke Guardian
|
|
private final LocationBroadcaster locationBroadcaster;
|
|
|
|
public LocationResponse updateLocation(Long userId, LocationUpdateRequest req) {
|
|
LocationHistory loc = LocationHistory.builder()
|
|
.userId(userId)
|
|
.lat(req.getLat())
|
|
.lng(req.getLng())
|
|
.accuracy(req.getAccuracy())
|
|
.speed(req.getSpeed())
|
|
.heading(req.getHeading())
|
|
.build();
|
|
loc = locationHistoryRepository.save(loc);
|
|
|
|
LocationResponse response = toResponse(loc);
|
|
|
|
// ✅ BROADCAST real-time ke Guardian via WebSocket
|
|
// Guardian subscribe ke /topic/location/{userId}
|
|
locationBroadcaster.broadcastLocation(userId, response);
|
|
log.debug("[LOCATION] Broadcast to Guardian | userId={} lat={} lng={}",
|
|
userId, req.getLat(), req.getLng());
|
|
|
|
// Cek geofence
|
|
checkGeofence(userId, req.getLat(), req.getLng());
|
|
|
|
return response;
|
|
}
|
|
|
|
public Optional<LocationResponse> getLastLocation(Long userId) {
|
|
return locationHistoryRepository
|
|
.findTopByUserIdOrderByCreatedAtDesc(userId)
|
|
.map(this::toResponse);
|
|
}
|
|
|
|
// Untuk Guardian: ambil last location user yang dipair
|
|
public Optional<LocationResponse> getLastLocationForGuardian(Long guardianId) {
|
|
return pairingRelationRepository
|
|
.findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE)
|
|
.flatMap(p -> locationHistoryRepository
|
|
.findTopByUserIdOrderByCreatedAtDesc(p.getUser().getId()))
|
|
.map(this::toResponse);
|
|
}
|
|
|
|
public Page<LocationResponse> getLocationHistory(Long userId, Pageable pageable) {
|
|
return locationHistoryRepository
|
|
.findByUserIdOrderByCreatedAtDesc(userId, pageable)
|
|
.map(this::toResponse);
|
|
}
|
|
|
|
public Page<LocationResponse> getLocationHistoryForGuardian(Long guardianId, Pageable pageable) {
|
|
Long pairedUserId = pairingRelationRepository
|
|
.findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE)
|
|
.map(pairing -> pairing.getUser().getId())
|
|
.orElseThrow(() -> new PairingException("Guardian belum terhubung dengan User aktif"));
|
|
|
|
return getLocationHistory(pairedUserId, pageable);
|
|
}
|
|
|
|
// ========== GEOFENCE ==========
|
|
|
|
private void checkGeofence(Long userId, double lat, double lng) {
|
|
geofenceConfigRepository.findByUserId(userId).ifPresent(cfg -> {
|
|
if (!cfg.getEnabled() || cfg.getCenterLat() == null) return;
|
|
|
|
double dist = haversineMeters(cfg.getCenterLat(), cfg.getCenterLng(), lat, lng);
|
|
|
|
if (dist > cfg.getRadiusMeters()) {
|
|
// User keluar geofence — beri tahu Guardian
|
|
pairingRelationRepository.findByUser_IdAndStatus(userId, PairingStatus.ACTIVE)
|
|
.ifPresent(pairing -> {
|
|
User guardian = pairing.getGuardian();
|
|
User user = pairing.getUser();
|
|
fcmService.sendHighPriority(
|
|
guardian.getFcmToken(),
|
|
"⚠️ User Keluar Area Aman",
|
|
user.getDisplayName() + " meninggalkan area geofence! " +
|
|
String.format("%.0fm dari pusat area", dist),
|
|
Map.of("type", "GEOFENCE_EXIT",
|
|
"userId", String.valueOf(userId),
|
|
"lat", String.valueOf(lat),
|
|
"lng", String.valueOf(lng))
|
|
);
|
|
activityLogService.createLog(user, ActivityLogType.GEOFENCE_EXIT,
|
|
String.format("User keluar area geofence (%.0fm dari pusat)", dist), null);
|
|
log.info("[GEOFENCE] User {} keluar area ({:.0f}m)", userId, dist);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Haversine formula — jarak antara 2 koordinat dalam meter */
|
|
public static double haversineMeters(double lat1, double lng1, double lat2, double lng2) {
|
|
final double R = 6371000; // radius bumi dalam meter
|
|
double dLat = Math.toRadians(lat2 - lat1);
|
|
double dLng = Math.toRadians(lng2 - lng1);
|
|
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
|
|
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
|
|
* Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
}
|
|
|
|
private LocationResponse toResponse(LocationHistory l) {
|
|
return LocationResponse.builder()
|
|
.id(l.getId()).lat(l.getLat()).lng(l.getLng())
|
|
.accuracy(l.getAccuracy()).speed(l.getSpeed()).heading(l.getHeading())
|
|
.createdAt(l.getCreatedAt()).build();
|
|
}
|
|
}
|