feat: Add native Android driver tracking app and real-time backend/frontend bus tracking system
This commit is contained in:
@@ -33,8 +33,10 @@ public class DataInitializer implements CommandLineRunner {
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
// Clear all previous mock data from the database
|
||||
// Clear all previous mock data from the database to force re-seeding with geocoded values
|
||||
noteRepository.deleteAll();
|
||||
busRouteRepository.deleteAll();
|
||||
|
||||
// Run initial scan to populate database with whatever is currently in /uploads
|
||||
try {
|
||||
notePyqController.syncUploads();
|
||||
@@ -44,50 +46,65 @@ public class DataInitializer implements CommandLineRunner {
|
||||
}
|
||||
|
||||
// Seed Bus Routes
|
||||
if (busRouteRepository.count() == 0) {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
InputStream is = getClass().getResourceAsStream("/bus_routes.json");
|
||||
if (is != null) {
|
||||
List<Map<String, Object>> routesList = mapper.readValue(is, new TypeReference<List<Map<String, Object>>>() {});
|
||||
List<BusRoute> routesToSave = new ArrayList<>();
|
||||
|
||||
for (Map<String, Object> routeMap : routesList) {
|
||||
BusRoute br = BusRoute.builder()
|
||||
.number((String) routeMap.get("number"))
|
||||
.name((String) routeMap.get("name"))
|
||||
.from((String) routeMap.get("from"))
|
||||
.to((String) routeMap.get("to"))
|
||||
.departureTime((String) routeMap.get("departureTime"))
|
||||
.arrivalTime((String) routeMap.get("arrivalTime"))
|
||||
.color((String) routeMap.get("color"))
|
||||
.build();
|
||||
|
||||
List<Map<String, String>> stopsList = (List<Map<String, String>>) routeMap.get("stops");
|
||||
List<BusStop> stops = new ArrayList<>();
|
||||
if (stopsList != null) {
|
||||
for (int i = 0; i < stopsList.size(); i++) {
|
||||
Map<String, String> stopMap = stopsList.get(i);
|
||||
stops.add(BusStop.builder()
|
||||
.route(br)
|
||||
.name(stopMap.get("name"))
|
||||
.time(stopMap.get("time"))
|
||||
.stopOrder(i + 1)
|
||||
.build());
|
||||
}
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
InputStream is = getClass().getResourceAsStream("/bus_routes.json");
|
||||
if (is != null) {
|
||||
List<Map<String, Object>> routesList = mapper.readValue(is, new TypeReference<List<Map<String, Object>>>() {});
|
||||
List<BusRoute> routesToSave = new ArrayList<>();
|
||||
|
||||
for (Map<String, Object> routeMap : routesList) {
|
||||
List<List<Double>> polyList = (List<List<Double>>) routeMap.get("polyline");
|
||||
String polyJson = null;
|
||||
if (polyList != null) {
|
||||
try {
|
||||
polyJson = mapper.writeValueAsString(polyList);
|
||||
} catch (Exception e) {
|
||||
System.err.println("⚠️ Failed to serialize polyline: " + e.getMessage());
|
||||
}
|
||||
br.setStops(stops);
|
||||
routesToSave.add(br);
|
||||
}
|
||||
busRouteRepository.saveAll(routesToSave);
|
||||
System.out.println("🌱 Database successfully seeded with " + routesToSave.size() + " Bus Routes and stops!");
|
||||
} else {
|
||||
System.err.println("⚠️ Could not find bus_routes.json in resources!");
|
||||
|
||||
BusRoute br = BusRoute.builder()
|
||||
.number((String) routeMap.get("number"))
|
||||
.name((String) routeMap.get("name"))
|
||||
.from((String) routeMap.get("from"))
|
||||
.to((String) routeMap.get("to"))
|
||||
.departureTime((String) routeMap.get("departureTime"))
|
||||
.arrivalTime((String) routeMap.get("arrivalTime"))
|
||||
.color((String) routeMap.get("color"))
|
||||
.fromLat(routeMap.get("from_lat") != null ? ((Number) routeMap.get("from_lat")).doubleValue() : null)
|
||||
.fromLng(routeMap.get("from_lng") != null ? ((Number) routeMap.get("from_lng")).doubleValue() : null)
|
||||
.toLat(routeMap.get("to_lat") != null ? ((Number) routeMap.get("to_lat")).doubleValue() : null)
|
||||
.toLng(routeMap.get("to_lng") != null ? ((Number) routeMap.get("to_lng")).doubleValue() : null)
|
||||
.polyline(polyJson)
|
||||
.build();
|
||||
|
||||
List<Map<String, Object>> stopsList = (List<Map<String, Object>>) routeMap.get("stops");
|
||||
List<BusStop> stops = new ArrayList<>();
|
||||
if (stopsList != null) {
|
||||
for (int i = 0; i < stopsList.size(); i++) {
|
||||
Map<String, Object> stopMap = stopsList.get(i);
|
||||
stops.add(BusStop.builder()
|
||||
.route(br)
|
||||
.name((String) stopMap.get("name"))
|
||||
.time((String) stopMap.get("time"))
|
||||
.lat(stopMap.get("lat") != null ? ((Number) stopMap.get("lat")).doubleValue() : null)
|
||||
.lng(stopMap.get("lng") != null ? ((Number) stopMap.get("lng")).doubleValue() : null)
|
||||
.stopOrder(i + 1)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
br.setStops(stops);
|
||||
routesToSave.add(br);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ Failed to seed bus routes: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
busRouteRepository.saveAll(routesToSave);
|
||||
System.out.println("🌱 Database successfully seeded with " + routesToSave.size() + " Bus Routes and stops!");
|
||||
} else {
|
||||
System.err.println("⚠️ Could not find bus_routes.json in resources!");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ Failed to seed bus routes: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.rit.portal.controller;
|
||||
|
||||
import com.rit.portal.dto.DriverLocationUpdate;
|
||||
import com.rit.portal.model.BusLocation;
|
||||
import com.rit.portal.service.BusLocationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/bus-locations")
|
||||
@CrossOrigin(originPatterns = "*")
|
||||
public class BusLocationController {
|
||||
|
||||
@Autowired
|
||||
private BusLocationService busLocationService;
|
||||
|
||||
private static final String REQUIRED_PIN = "RITDRIVER";
|
||||
|
||||
@PostMapping("/{routeNumber}")
|
||||
public ResponseEntity<Map<String, String>> updateLocation(
|
||||
@PathVariable String routeNumber,
|
||||
@RequestBody DriverLocationUpdate update) {
|
||||
|
||||
if (update == null || update.getLatitude() == null || update.getLongitude() == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid coordinates"));
|
||||
}
|
||||
|
||||
if (!REQUIRED_PIN.equals(update.getPin())) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("error", "Invalid driver PIN"));
|
||||
}
|
||||
|
||||
busLocationService.updateLocation(routeNumber, update.getLatitude(), update.getLongitude());
|
||||
return ResponseEntity.ok(Map.of("status", "success", "message", "Location updated successfully"));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<BusLocation>> getActiveLocations() {
|
||||
return ResponseEntity.ok(busLocationService.getActiveLocations());
|
||||
}
|
||||
|
||||
@GetMapping("/{routeNumber}")
|
||||
public ResponseEntity<BusLocation> getLocation(@PathVariable String routeNumber) {
|
||||
BusLocation loc = busLocationService.getLocation(routeNumber);
|
||||
if (loc == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(loc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.rit.portal.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DriverLocationUpdate {
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private String pin;
|
||||
}
|
||||
@@ -39,6 +39,22 @@ public class BusRoute {
|
||||
@Column(name = "color_code")
|
||||
private String color;
|
||||
|
||||
@Column(name = "from_lat")
|
||||
private Double fromLat;
|
||||
|
||||
@Column(name = "from_lng")
|
||||
private Double fromLng;
|
||||
|
||||
@Column(name = "to_lat")
|
||||
private Double toLat;
|
||||
|
||||
@Column(name = "to_lng")
|
||||
private Double toLng;
|
||||
|
||||
@Column(name = "polyline_data", columnDefinition = "TEXT")
|
||||
@com.fasterxml.jackson.annotation.JsonRawValue
|
||||
private String polyline;
|
||||
|
||||
@OneToMany(mappedBy = "route", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
|
||||
@OrderBy("stopOrder ASC")
|
||||
@Builder.Default
|
||||
|
||||
@@ -28,6 +28,12 @@ public class BusStop {
|
||||
@Column(name = "arrival_time", nullable = false)
|
||||
private String time;
|
||||
|
||||
@Column(name = "latitude")
|
||||
private Double lat;
|
||||
|
||||
@Column(name = "longitude")
|
||||
private Double lng;
|
||||
|
||||
@Column(name = "stop_order", nullable = false)
|
||||
private Integer stopOrder;
|
||||
}
|
||||
|
||||
16
backend/src/main/java/com/rit/portal/model/BusLocation.java
Normal file
16
backend/src/main/java/com/rit/portal/model/BusLocation.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.rit.portal.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BusLocation {
|
||||
private String routeNumber;
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private Instant lastUpdated;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.rit.portal.service;
|
||||
|
||||
import com.rit.portal.model.BusLocation;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.time.Instant;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class BusLocationService {
|
||||
|
||||
private final Map<String, BusLocation> locationCache = new ConcurrentHashMap<>();
|
||||
private static final Duration STALE_DURATION = Duration.ofMinutes(2);
|
||||
|
||||
public void updateLocation(String routeNumber, Double latitude, Double longitude) {
|
||||
locationCache.put(routeNumber, new BusLocation(
|
||||
routeNumber,
|
||||
latitude,
|
||||
longitude,
|
||||
Instant.now()
|
||||
));
|
||||
}
|
||||
|
||||
public BusLocation getLocation(String routeNumber) {
|
||||
BusLocation loc = locationCache.get(routeNumber);
|
||||
if (loc != null) {
|
||||
if (Instant.now().isAfter(loc.getLastUpdated().plus(STALE_DURATION))) {
|
||||
locationCache.remove(routeNumber);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return loc;
|
||||
}
|
||||
|
||||
public List<BusLocation> getActiveLocations() {
|
||||
Instant threshold = Instant.now().minus(STALE_DURATION);
|
||||
// Clean up stale entries on read
|
||||
locationCache.entrySet().removeIf(entry -> entry.getValue().getLastUpdated().isBefore(threshold));
|
||||
return List.copyOf(locationCache.values());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user