2026-05-26 05:52:12 +07:00

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();
}
}