Remove all markers and handle 20 min location sharing stopped state
This commit is contained in:
@@ -12,7 +12,7 @@ import java.util.List;
|
|||||||
public class BusLocationService {
|
public class BusLocationService {
|
||||||
|
|
||||||
private final Map<String, BusLocation> locationCache = new ConcurrentHashMap<>();
|
private final Map<String, BusLocation> locationCache = new ConcurrentHashMap<>();
|
||||||
private static final Duration STALE_DURATION = Duration.ofMinutes(2);
|
private static final Duration STALE_DURATION = Duration.ofMinutes(20);
|
||||||
|
|
||||||
public void updateLocation(String routeNumber, Double latitude, Double longitude) {
|
public void updateLocation(String routeNumber, Double latitude, Double longitude) {
|
||||||
locationCache.put(routeNumber, new BusLocation(
|
locationCache.put(routeNumber, new BusLocation(
|
||||||
|
|||||||
@@ -46,30 +46,17 @@ const isRealStopCoordinate = (stop: { name: string; lat?: number; lng?: number }
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Custom Premium DivIcons using Tailwind CSS
|
|
||||||
const getStartIcon = (label: string = "1") => L.divIcon({
|
|
||||||
html: `<div class="flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500 border-2 border-white shadow-md text-white text-[10px] font-extrabold hover:scale-110 transition-transform">${label}</div>`,
|
|
||||||
className: '',
|
|
||||||
iconSize: [24, 24],
|
|
||||||
iconAnchor: [12, 12],
|
|
||||||
});
|
|
||||||
|
|
||||||
const getStopIcon = (num: number) => L.divIcon({
|
|
||||||
html: `<div class="flex items-center justify-center w-5.5 h-5.5 rounded-full bg-blue-600 border-2 border-white shadow-md text-white text-[10px] font-bold hover:scale-125 hover:bg-blue-700 transition-all">${num}</div>`,
|
|
||||||
className: '',
|
|
||||||
iconSize: [22, 22],
|
|
||||||
iconAnchor: [11, 11],
|
|
||||||
});
|
|
||||||
|
|
||||||
const campusIcon = L.divIcon({
|
const liveBusIcon = L.divIcon({
|
||||||
html: `<div class="flex items-center justify-center w-9 h-9 rounded-full bg-gradient-to-tr from-orange-500 to-amber-500 border-2 border-white shadow-lg text-white text-sm font-bold animate-pulse hover:scale-110 transition-transform">🏫</div>`,
|
html: `<div class="relative flex items-center justify-center w-9 h-9 rounded-full bg-emerald-500 border-2 border-white shadow-lg text-white text-sm font-bold"><span class="absolute inset-0 rounded-full bg-emerald-500/40 animate-ping"></span>🚌</div>`,
|
||||||
className: '',
|
className: '',
|
||||||
iconSize: [36, 36],
|
iconSize: [36, 36],
|
||||||
iconAnchor: [18, 18],
|
iconAnchor: [18, 18],
|
||||||
});
|
});
|
||||||
|
|
||||||
const liveBusIcon = L.divIcon({
|
const stoppedBusIcon = L.divIcon({
|
||||||
html: `<div class="relative flex items-center justify-center w-9 h-9 rounded-full bg-emerald-500 border-2 border-white shadow-lg text-white text-sm font-bold"><span class="absolute inset-0 rounded-full bg-emerald-500/40 animate-ping"></span>🚌</div>`,
|
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>`,
|
||||||
className: '',
|
className: '',
|
||||||
iconSize: [36, 36],
|
iconSize: [36, 36],
|
||||||
iconAnchor: [18, 18],
|
iconAnchor: [18, 18],
|
||||||
@@ -94,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 }>>({});
|
const [allLiveLocations, setAllLiveLocations] = useState<Record<string, { latitude: number; longitude: number; lastUpdated: string }>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLiveLocations = () => {
|
const fetchLiveLocations = () => {
|
||||||
@@ -103,12 +90,12 @@ 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 }>) => {
|
.then((data: Array<{ routeNumber: string; latitude: number; longitude: number; lastUpdated: string }>) => {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
const locMap: Record<string, { latitude: number; longitude: number }> = {};
|
const locMap: Record<string, { latitude: number; longitude: number; lastUpdated: string }> = {};
|
||||||
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 };
|
locMap[item.routeNumber] = { latitude: item.latitude, longitude: item.longitude, lastUpdated: item.lastUpdated };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setAllLiveLocations(locMap);
|
setAllLiveLocations(locMap);
|
||||||
@@ -175,83 +162,7 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
|
|||||||
return pathCoordinates as L.LatLngBoundsExpression;
|
return pathCoordinates as L.LatLngBoundsExpression;
|
||||||
}, [pathCoordinates]);
|
}, [pathCoordinates]);
|
||||||
|
|
||||||
// Determine which markers to display
|
|
||||||
const renderMarkers = () => {
|
|
||||||
if (selectedRoute) {
|
|
||||||
// Filter out stops that do not have valid/real coordinates
|
|
||||||
const validStops = selectedRoute.stops.filter(stop => isRealStopCoordinate(stop));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{validStops.map((stop, index) => {
|
|
||||||
const isStart = index === 0;
|
|
||||||
const isEnd = index === validStops.length - 1;
|
|
||||||
const isRit = stop.name.toLowerCase().includes("rit");
|
|
||||||
|
|
||||||
let currentIcon = getStopIcon(index + 1);
|
|
||||||
if (isStart) currentIcon = getStartIcon("Start");
|
|
||||||
if (isRit || isEnd) currentIcon = campusIcon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Marker
|
|
||||||
key={`${stop.name}-${index}`}
|
|
||||||
position={[stop.lat!, stop.lng!]}
|
|
||||||
icon={currentIcon}
|
|
||||||
>
|
|
||||||
<Popup>
|
|
||||||
<div className="p-1 font-sans">
|
|
||||||
<div className="flex items-center gap-1.5 mb-0.5">
|
|
||||||
<span className="text-[10px] bg-slate-100 text-slate-700 px-1.5 py-0.5 rounded font-bold">
|
|
||||||
Stop #{index + 1}
|
|
||||||
</span>
|
|
||||||
<h4 className="font-bold text-slate-800 text-xs">{stop.name}</h4>
|
|
||||||
</div>
|
|
||||||
<p className="text-[10px] text-orange-500 font-semibold mt-0.5">Estimated Arrival: {stop.time}</p>
|
|
||||||
{isStart && <span className="inline-block text-[9px] bg-emerald-100 text-emerald-700 px-1.5 py-0.5 rounded-full mt-1 font-bold">Departure Stop</span>}
|
|
||||||
{isRit && <span className="inline-block text-[9px] bg-orange-100 text-orange-700 px-1.5 py-0.5 rounded-full mt-1 font-bold">RIT Campus</span>}
|
|
||||||
</div>
|
|
||||||
</Popup>
|
|
||||||
</Marker>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default view: Draw RIT Campus pin + Start pins for all routes (only if coordinates are valid)
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Marker position={[13.0118, 80.0214]} icon={campusIcon}>
|
|
||||||
<Popup>
|
|
||||||
<div className="p-1 font-sans">
|
|
||||||
<h4 className="font-bold text-slate-800 text-xs">Rajalakshmi Institute of Technology</h4>
|
|
||||||
<p className="text-[10px] text-slate-500 mt-0.5">Kuthambakkam, Chennai</p>
|
|
||||||
</div>
|
|
||||||
</Popup>
|
|
||||||
</Marker>
|
|
||||||
{allRoutes.map((r, i) => {
|
|
||||||
if (!isValidCoordinate(r.from_lat, r.from_lng)) return null;
|
|
||||||
return (
|
|
||||||
<Marker
|
|
||||||
key={`start-${r.number}-${i}`}
|
|
||||||
position={[r.from_lat!, r.from_lng!]}
|
|
||||||
icon={getStartIcon(r.number)}
|
|
||||||
>
|
|
||||||
<Popup>
|
|
||||||
<div className="p-1 font-sans">
|
|
||||||
<span className="px-2 py-0.5 rounded text-[10px] text-white font-bold" style={{ backgroundColor: r.color }}>
|
|
||||||
{r.number}
|
|
||||||
</span>
|
|
||||||
<h4 className="font-bold text-slate-800 text-xs mt-1">{r.name}</h4>
|
|
||||||
<p className="text-[10px] text-slate-500">Starts: {r.from} at {r.departureTime}</p>
|
|
||||||
</div>
|
|
||||||
</Popup>
|
|
||||||
</Marker>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full min-h-[350px] bg-slate-950 rounded-2xl overflow-hidden border border-slate-200/80 shadow-md">
|
<div className="relative h-full w-full min-h-[350px] bg-slate-950 rounded-2xl overflow-hidden border border-slate-200/80 shadow-md">
|
||||||
@@ -266,23 +177,32 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
|
|||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{renderMarkers()}
|
|
||||||
|
|
||||||
{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;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
key={`live-bus-${rNum}`}
|
key={`live-bus-${rNum}`}
|
||||||
position={[loc.latitude, loc.longitude]}
|
position={[loc.latitude, loc.longitude]}
|
||||||
icon={liveBusIcon}
|
icon={isStale ? stoppedBusIcon : liveBusIcon}
|
||||||
>
|
>
|
||||||
<Popup>
|
<Popup>
|
||||||
<div className="p-1 font-sans text-center">
|
<div className="p-1 font-sans text-center">
|
||||||
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-800 text-[10px] font-bold">
|
{isStale ? (
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>LIVE TRACKING
|
<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>
|
LOCATION SHARING STOPPED
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-800 text-[10px] font-bold">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>LIVE TRACKING
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<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">Broadcasting live coordinates</p>
|
<p className="text-[10px] text-slate-500 mt-0.5">
|
||||||
|
{isStale ? "Last known location" : "Broadcasting live coordinates"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
</Marker>
|
</Marker>
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user