Implement active Location Sharing Stopped/Ended state with red dot pin
This commit is contained in:
@@ -37,6 +37,20 @@ public class BusLocationController {
|
|||||||
return ResponseEntity.ok(Map.of("status", "success", "message", "Location updated successfully"));
|
return ResponseEntity.ok(Map.of("status", "success", "message", "Location updated successfully"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{routeNumber}/stop")
|
||||||
|
public ResponseEntity<Map<String, String>> stopLocation(
|
||||||
|
@PathVariable String routeNumber,
|
||||||
|
@RequestBody Map<String, String> body) {
|
||||||
|
|
||||||
|
String pin = body != null ? body.get("pin") : null;
|
||||||
|
if (!REQUIRED_PIN.equals(pin)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("error", "Invalid driver PIN"));
|
||||||
|
}
|
||||||
|
|
||||||
|
busLocationService.stopLocation(routeNumber);
|
||||||
|
return ResponseEntity.ok(Map.of("status", "success", "message", "Location sharing stopped"));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<List<BusLocation>> getActiveLocations() {
|
public ResponseEntity<List<BusLocation>> getActiveLocations() {
|
||||||
return ResponseEntity.ok(busLocationService.getActiveLocations());
|
return ResponseEntity.ok(busLocationService.getActiveLocations());
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ public class BusLocation {
|
|||||||
private Double latitude;
|
private Double latitude;
|
||||||
private Double longitude;
|
private Double longitude;
|
||||||
private Instant lastUpdated;
|
private Instant lastUpdated;
|
||||||
|
private boolean stopped = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,19 @@ public class BusLocationService {
|
|||||||
routeNumber,
|
routeNumber,
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
Instant.now()
|
Instant.now(),
|
||||||
|
false
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void stopLocation(String routeNumber) {
|
||||||
|
BusLocation loc = locationCache.get(routeNumber);
|
||||||
|
if (loc != null) {
|
||||||
|
loc.setStopped(true);
|
||||||
|
loc.setLastUpdated(Instant.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public BusLocation getLocation(String routeNumber) {
|
public BusLocation getLocation(String routeNumber) {
|
||||||
BusLocation loc = locationCache.get(routeNumber);
|
BusLocation loc = locationCache.get(routeNumber);
|
||||||
if (loc != null) {
|
if (loc != null) {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const liveBusIcon = L.divIcon({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const stoppedBusIcon = L.divIcon({
|
const stoppedBusIcon = L.divIcon({
|
||||||
html: `<div class="relative flex items-center justify-center w-9 h-9 rounded-full bg-slate-500 border-2 border-white shadow-lg text-white text-sm font-bold">🚌</div>`,
|
html: `<div class="relative flex items-center justify-center w-9 h-9 rounded-full bg-red-500 border-2 border-white shadow-lg text-white text-sm font-bold">🚌</div>`,
|
||||||
className: '',
|
className: '',
|
||||||
iconSize: [36, 36],
|
iconSize: [36, 36],
|
||||||
iconAnchor: [18, 18],
|
iconAnchor: [18, 18],
|
||||||
@@ -81,7 +81,7 @@ interface BusRouteMapProps {
|
|||||||
const DEFAULT_CENTER = [13.0118, 80.0214]; // RIT Campus default
|
const DEFAULT_CENTER = [13.0118, 80.0214]; // RIT Campus default
|
||||||
|
|
||||||
export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapProps) {
|
export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapProps) {
|
||||||
const [allLiveLocations, setAllLiveLocations] = useState<Record<string, { latitude: number; longitude: number; lastUpdated: string }>>({});
|
const [allLiveLocations, setAllLiveLocations] = useState<Record<string, { latitude: number; longitude: number; lastUpdated: string; stopped: boolean }>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLiveLocations = () => {
|
const fetchLiveLocations = () => {
|
||||||
@@ -90,12 +90,17 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
|
|||||||
if (res.ok) return res.json();
|
if (res.ok) return res.json();
|
||||||
return [];
|
return [];
|
||||||
})
|
})
|
||||||
.then((data: Array<{ routeNumber: string; latitude: number; longitude: number; lastUpdated: string }>) => {
|
.then((data: Array<{ routeNumber: string; latitude: number; longitude: number; lastUpdated: string; stopped: boolean }>) => {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
const locMap: Record<string, { latitude: number; longitude: number; lastUpdated: string }> = {};
|
const locMap: Record<string, { latitude: number; longitude: number; lastUpdated: string; stopped: boolean }> = {};
|
||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
if (item.routeNumber && item.latitude && item.longitude) {
|
if (item.routeNumber && item.latitude && item.longitude) {
|
||||||
locMap[item.routeNumber] = { latitude: item.latitude, longitude: item.longitude, lastUpdated: item.lastUpdated };
|
locMap[item.routeNumber] = {
|
||||||
|
latitude: item.latitude,
|
||||||
|
longitude: item.longitude,
|
||||||
|
lastUpdated: item.lastUpdated,
|
||||||
|
stopped: item.stopped
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setAllLiveLocations(locMap);
|
setAllLiveLocations(locMap);
|
||||||
@@ -180,7 +185,7 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
|
|||||||
{Object.entries(allLiveLocations).map(([rNum, loc]) => {
|
{Object.entries(allLiveLocations).map(([rNum, loc]) => {
|
||||||
if (selectedRoute && selectedRoute.number !== rNum) return null;
|
if (selectedRoute && selectedRoute.number !== rNum) return null;
|
||||||
|
|
||||||
const isStale = loc.lastUpdated ? (new Date().getTime() - new Date(loc.lastUpdated).getTime() > 120000) : false;
|
const isStale = loc.stopped || (loc.lastUpdated ? (new Date().getTime() - new Date(loc.lastUpdated).getTime() > 120000) : false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
@@ -190,7 +195,11 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
|
|||||||
>
|
>
|
||||||
<Popup>
|
<Popup>
|
||||||
<div className="p-1 font-sans text-center">
|
<div className="p-1 font-sans text-center">
|
||||||
{isStale ? (
|
{loc.stopped ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-red-100 text-red-800 text-[10px] font-bold">
|
||||||
|
LOCATION SHARING ENDED
|
||||||
|
</span>
|
||||||
|
) : isStale ? (
|
||||||
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-slate-100 text-slate-800 text-[10px] font-bold">
|
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-slate-100 text-slate-800 text-[10px] font-bold">
|
||||||
LOCATION SHARING STOPPED
|
LOCATION SHARING STOPPED
|
||||||
</span>
|
</span>
|
||||||
@@ -201,7 +210,7 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
|
|||||||
)}
|
)}
|
||||||
<h4 className="font-bold text-slate-800 text-xs mt-1">Bus {rNum}</h4>
|
<h4 className="font-bold text-slate-800 text-xs mt-1">Bus {rNum}</h4>
|
||||||
<p className="text-[10px] text-slate-500 mt-0.5">
|
<p className="text-[10px] text-slate-500 mt-0.5">
|
||||||
{isStale ? "Last known location" : "Broadcasting live coordinates"}
|
{loc.stopped ? "Sharing ended by driver" : isStale ? "Last known location" : "Broadcasting live coordinates"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|||||||
Reference in New Issue
Block a user