Compare commits

..
4 Commits
Author SHA1 Message Date
cjdenio 03ded9250d add README 2026-09-29 14:53:13 -04:00
cjdenio 74eb8e55e3 small improvement 2026-09-22 16:54:26 -06:00
cjdenio f84c2143b8 wildcat shuttle 2026-09-22 10:23:44 -06:00
cjdenio b0c522d994 updates 2026-09-21 22:19:16 -06:00
7 changed files with 83 additions and 18 deletions

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
# UTA tracker
Tracking all the trains & buses in Salt Lake City.
## API
- `GET https://uta.clb.li/api.json`: List all vehicles currently in service (JSON)
- `GET https://uta.clb.li/api`: Same as above, but Protobuf (see ![proto/schema.proto](./proto/schema.proto))
- `GET https://uta.clb.li/api/shapes/{shape_id}`: Get the geographic route "shape" for a given vehicle's `shape_id`
![screenshot](screenshot.png)
+37 -2
View File
@@ -134,6 +134,7 @@ func feedifyVehicles(vehicles []*pb.VehiclePosition, header *pb.FeedHeader, rout
rows, err := scheduleDb.Query(` rows, err := scheduleDb.Query(`
SELECT SELECT
trips.route_id, trips.route_id,
trips.shape_id,
trips.trip_headsign, trips.trip_headsign,
routes.route_type, routes.route_type,
routes.route_color, routes.route_color,
@@ -150,6 +151,7 @@ func feedifyVehicles(vehicles []*pb.VehiclePosition, header *pb.FeedHeader, rout
} }
var route_id string var route_id string
var shape_id string
var trip_headsign sql.NullString var trip_headsign sql.NullString
var route_type int32 var route_type int32
var route_color sql.NullString var route_color sql.NullString
@@ -160,7 +162,7 @@ func feedifyVehicles(vehicles []*pb.VehiclePosition, header *pb.FeedHeader, rout
continue continue
} }
err = rows.Scan(&route_id, &trip_headsign, &route_type, &route_color, &route_short_name, &route_long_name) err = rows.Scan(&route_id, &shape_id, &trip_headsign, &route_type, &route_color, &route_short_name, &route_long_name)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
continue continue
@@ -195,7 +197,8 @@ func feedifyVehicles(vehicles []*pb.VehiclePosition, header *pb.FeedHeader, rout
ShortName: route_short_name.String, ShortName: route_short_name.String,
LongName: route_long_name.String, LongName: route_long_name.String,
}, },
TripId: *vehicle.Trip.TripId, TripId: *vehicle.Trip.TripId,
ShapeId: shape_id,
}) })
} }
@@ -288,6 +291,37 @@ func tripHandler(w http.ResponseWriter, r *http.Request) {
w.Write(response) w.Write(response)
} }
func shapeHandler(w http.ResponseWriter, r *http.Request) {
rows, err := scheduleDb.Query(`SELECT shapes.shape_pt_lat, shapes.shape_pt_lon FROM shapes WHERE shapes.shape_id = ? ORDER BY shapes.shape_pt_sequence ASC`, r.PathValue("shape"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
var points [][]float64
for rows.Next() {
var lat float64
var lon float64
err = rows.Scan(&lat, &lon)
if err != nil {
break
}
points = append(points, []float64{lat, lon})
}
if len(points) == 0 {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Add("Content-Type", "application/json")
response, _ := json.Marshal(map[string]any{"shape": points})
w.Write(response)
}
func main() { func main() {
fmt.Println("Opening database...") fmt.Println("Opening database...")
@@ -307,6 +341,7 @@ func main() {
http.HandleFunc("/api", vehicleHandler("")) http.HandleFunc("/api", vehicleHandler(""))
http.HandleFunc("/api.json", vehicleHandler("json")) http.HandleFunc("/api.json", vehicleHandler("json"))
http.HandleFunc("/api/trips/{trip}", tripHandler) http.HandleFunc("/api/trips/{trip}", tripHandler)
http.HandleFunc("/api/shapes/{shape}", shapeHandler)
port := "3000" port := "3000"
if portEnv, ok := os.LookupEnv("PORT"); ok { if portEnv, ok := os.LookupEnv("PORT"); ok {
+12 -3
View File
@@ -386,6 +386,7 @@ type VehicleFeed_Vehicle struct {
Headsign string `protobuf:"bytes,7,opt,name=headsign,proto3" json:"headsign,omitempty"` Headsign string `protobuf:"bytes,7,opt,name=headsign,proto3" json:"headsign,omitempty"`
Route *VehicleFeed_Route `protobuf:"bytes,8,opt,name=route,proto3" json:"route,omitempty"` Route *VehicleFeed_Route `protobuf:"bytes,8,opt,name=route,proto3" json:"route,omitempty"`
TripId string `protobuf:"bytes,10,opt,name=trip_id,json=tripId,proto3" json:"trip_id,omitempty"` TripId string `protobuf:"bytes,10,opt,name=trip_id,json=tripId,proto3" json:"trip_id,omitempty"`
ShapeId string `protobuf:"bytes,11,opt,name=shape_id,json=shapeId,proto3" json:"shape_id,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -490,11 +491,18 @@ func (x *VehicleFeed_Vehicle) GetTripId() string {
return "" return ""
} }
func (x *VehicleFeed_Vehicle) GetShapeId() string {
if x != nil {
return x.ShapeId
}
return ""
}
var File_proto_schema_proto protoreflect.FileDescriptor var File_proto_schema_proto protoreflect.FileDescriptor
const file_proto_schema_proto_rawDesc = "" + const file_proto_schema_proto_rawDesc = "" +
"\n" + "\n" +
"\x12proto/schema.proto\"\xe8\x06\n" + "\x12proto/schema.proto\"\x83\a\n" +
"\vVehicleFeed\x120\n" + "\vVehicleFeed\x120\n" +
"\bvehicles\x18\x01 \x03(\v2\x14.VehicleFeed.VehicleR\bvehicles\x12)\n" + "\bvehicles\x18\x01 \x03(\v2\x14.VehicleFeed.VehicleR\bvehicles\x12)\n" +
"\x04info\x18\x02 \x01(\v2\x15.VehicleFeed.FeedInfoR\x04info\x1a+\n" + "\x04info\x18\x02 \x01(\v2\x15.VehicleFeed.FeedInfoR\x04info\x1a+\n" +
@@ -519,7 +527,7 @@ const file_proto_schema_proto_rawDesc = "" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x10\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x10\n" +
"\x03lat\x18\x03 \x01(\x02R\x03lat\x12\x10\n" + "\x03lat\x18\x03 \x01(\x02R\x03lat\x12\x10\n" +
"\x03lon\x18\x04 \x01(\x02R\x03lon\x1a\xba\x02\n" + "\x03lon\x18\x04 \x01(\x02R\x03lon\x1a\xd5\x02\n" +
"\aVehicle\x12\x10\n" + "\aVehicle\x12\x10\n" +
"\x03lat\x18\x01 \x01(\x02R\x03lat\x12\x10\n" + "\x03lat\x18\x01 \x01(\x02R\x03lat\x12\x10\n" +
"\x03lon\x18\x02 \x01(\x02R\x03lon\x12\x18\n" + "\x03lon\x18\x02 \x01(\x02R\x03lon\x12\x18\n" +
@@ -531,7 +539,8 @@ const file_proto_schema_proto_rawDesc = "" +
"\bheadsign\x18\a \x01(\tR\bheadsign\x12(\n" + "\bheadsign\x18\a \x01(\tR\bheadsign\x12(\n" +
"\x05route\x18\b \x01(\v2\x12.VehicleFeed.RouteR\x05route\x12\x17\n" + "\x05route\x18\b \x01(\v2\x12.VehicleFeed.RouteR\x05route\x12\x17\n" +
"\atrip_id\x18\n" + "\atrip_id\x18\n" +
" \x01(\tR\x06tripId\"Z\n" + " \x01(\tR\x06tripId\x12\x19\n" +
"\bshape_id\x18\v \x01(\tR\ashapeId\"Z\n" +
"\x04Line\x12\x14\n" + "\x04Line\x12\x14\n" +
"\x10LINE_UNSPECIFIED\x10\x00\x12\t\n" + "\x10LINE_UNSPECIFIED\x10\x00\x12\t\n" +
"\x05GREEN\x10\x01\x12\a\n" + "\x05GREEN\x10\x01\x12\a\n" +
+2
View File
@@ -1,4 +1,5 @@
syntax = "proto3"; syntax = "proto3";
option go_package = "github.com/cjdenio/uta-trax-api/proto"; option go_package = "github.com/cjdenio/uta-trax-api/proto";
message VehicleFeed { message VehicleFeed {
@@ -49,6 +50,7 @@ message VehicleFeed {
string headsign = 7; string headsign = 7;
Route route = 8; Route route = 8;
string trip_id = 10; string trip_id = 10;
string shape_id = 11;
} }
repeated Vehicle vehicles = 1; repeated Vehicle vehicles = 1;
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

+1 -1
View File
@@ -300,6 +300,6 @@
</div> </div>
</div> </div>
<script type="module" src="/js/main.js?v=11"></script> <script type="module" src="/js/main.js?v=14"></script>
</body> </body>
</html> </html>
+20 -12
View File
@@ -13,6 +13,13 @@ function displayLastUpdated() {
setInterval(displayLastUpdated, 1000) setInterval(displayLastUpdated, 1000)
const BRT_ROUTES = [
"92235", // OGX
"3686", // UVX
"87711", // MVX
"90585", // OGX Wildcat Shuttle
]
const RouteType = { const RouteType = {
TRAM: 1, TRAM: 1,
RAIL: 3, RAIL: 3,
@@ -142,6 +149,8 @@ function clearMap() {
function routeDesignator(route) { function routeDesignator(route) {
if (route.id == "92235") { if (route.id == "92235") {
return "OGX"; return "OGX";
} else if (route.id == "90585") {
return "OGX Wildcat Shuttle";
} else if (route.id == "3686") { } else if (route.id == "3686") {
return "UVX"; return "UVX";
} else if (route.id == "87711") { } else if (route.id == "87711") {
@@ -206,7 +215,7 @@ function renderVehicleIcon(vehicle) {
className: "", className: "",
html: `<div class="vehicle ${ html: `<div class="vehicle ${
vehicle.route.type == RouteType.BUS && vehicle.route.type == RouteType.BUS &&
!["92235", "3686", "87711"].includes(vehicle.route.id) !BRT_ROUTES.includes(vehicle.route.id)
? "vehicle-plain" ? "vehicle-plain"
: "" : ""
} ${ } ${
@@ -246,12 +255,12 @@ function vehiclePopupContent(vehicle) {
const tripShapeCache = new Map(); const tripShapeCache = new Map();
async function getTripShape(tripId) { async function getTripShape(shapeId) {
if (tripShapeCache.has(tripId)) { if (tripShapeCache.has(shapeId)) {
return tripShapeCache.get(tripId); return tripShapeCache.get(shapeId);
} else { } else {
const data = await fetch(`/api/trips/${tripId}`).then(r => r.json()); const data = await fetch(`/api/shapes/${shapeId}`).then(r => r.json());
tripShapeCache.set(tripId, data.shape); tripShapeCache.set(shapeId, data.shape);
return data.shape; return data.shape;
} }
} }
@@ -265,11 +274,10 @@ function renderVehicle(vehicle) {
: 3000, : 3000,
icon: renderVehicleIcon(vehicle), icon: renderVehicleIcon(vehicle),
}).bindPopup(vehiclePopupContent(vehicle)).on("popupopen", () => { }).bindPopup(vehiclePopupContent(vehicle)).on("popupopen", () => {
getTripShape(vehicle.trip_id).then(shape => { for (const layer of railLayer.getLayers()) {
console.log(shape) layer.setStyle({ opacity: 0.2 });
for (const layer of railLayer.getLayers()) { }
layer.setStyle({ opacity: 0.2 }); getTripShape(vehicle.shape_id).then(shape => {
}
L.polyline(shape, { color: `#${vehicle.route.color}`, zIndexOffset: 10 }).addTo(currentRouteLayer); L.polyline(shape, { color: `#${vehicle.route.color}`, zIndexOffset: 10 }).addTo(currentRouteLayer);
}) })
}).on("popupclose", () => { }).on("popupclose", () => {
@@ -312,7 +320,7 @@ async function reload() {
currentVehicles.get(vehicle.id).marker.setPopupContent(vehiclePopupContent(vehicle)) currentVehicles.get(vehicle.id).marker.setPopupContent(vehiclePopupContent(vehicle))
} else { } else {
const marker = renderVehicle(vehicle) const marker = renderVehicle(vehicle)
const layer = ["92235", "3686", "87711"].includes(vehicle.route.id) const layer = BRT_ROUTES.includes(vehicle.route.id)
? brtLayer ? brtLayer
: vehicle.route.type == RouteType.TRAM : vehicle.route.type == RouteType.TRAM
? traxLayer ? traxLayer