mirror of
https://github.com/cjdenio/uta-trax-api.git
synced 2026-10-03 16:06:19 +00:00
Compare commits
8
Commits
b6d4b6af70
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03ded9250d | ||
|
|
74eb8e55e3 | ||
|
|
f84c2143b8 | ||
|
|
b0c522d994 | ||
|
|
ca0939ee67 | ||
|
|
b7321adbe4 | ||
|
|
0831e523fa | ||
|
|
99bc1d595f |
No files matched your search
@@ -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 )
|
||||
- `GET https://uta.clb.li/api/shapes/{shape_id}`: Get the geographic route "shape" for a given vehicle's `shape_id`
|
||||
|
||||

|
||||
+68
-1
@@ -134,6 +134,7 @@ func feedifyVehicles(vehicles []*pb.VehiclePosition, header *pb.FeedHeader, rout
|
||||
rows, err := scheduleDb.Query(`
|
||||
SELECT
|
||||
trips.route_id,
|
||||
trips.shape_id,
|
||||
trips.trip_headsign,
|
||||
routes.route_type,
|
||||
routes.route_color,
|
||||
@@ -150,6 +151,7 @@ func feedifyVehicles(vehicles []*pb.VehiclePosition, header *pb.FeedHeader, rout
|
||||
}
|
||||
|
||||
var route_id string
|
||||
var shape_id string
|
||||
var trip_headsign sql.NullString
|
||||
var route_type int32
|
||||
var route_color sql.NullString
|
||||
@@ -160,7 +162,7 @@ func feedifyVehicles(vehicles []*pb.VehiclePosition, header *pb.FeedHeader, rout
|
||||
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 {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
@@ -196,6 +198,7 @@ func feedifyVehicles(vehicles []*pb.VehiclePosition, header *pb.FeedHeader, rout
|
||||
LongName: route_long_name.String,
|
||||
},
|
||||
TripId: *vehicle.Trip.TripId,
|
||||
ShapeId: shape_id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -257,6 +260,68 @@ func vehicleHandler(mode string) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func tripHandler(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := scheduleDb.Query(`SELECT shapes.shape_pt_lat, shapes.shape_pt_lon FROM shapes INNER JOIN trips ON trips.shape_id = shapes.shape_id WHERE trips.trip_id = ? ORDER BY shapes.shape_pt_sequence ASC`, r.PathValue("trip"))
|
||||
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 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() {
|
||||
fmt.Println("Opening database...")
|
||||
|
||||
@@ -275,6 +340,8 @@ func main() {
|
||||
|
||||
http.HandleFunc("/api", vehicleHandler(""))
|
||||
http.HandleFunc("/api.json", vehicleHandler("json"))
|
||||
http.HandleFunc("/api/trips/{trip}", tripHandler)
|
||||
http.HandleFunc("/api/shapes/{shape}", shapeHandler)
|
||||
|
||||
port := "3000"
|
||||
if portEnv, ok := os.LookupEnv("PORT"); ok {
|
||||
|
||||
+12
-3
@@ -386,6 +386,7 @@ type VehicleFeed_Vehicle struct {
|
||||
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"`
|
||||
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
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -490,11 +491,18 @@ func (x *VehicleFeed_Vehicle) GetTripId() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *VehicleFeed_Vehicle) GetShapeId() string {
|
||||
if x != nil {
|
||||
return x.ShapeId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_proto_schema_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proto_schema_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x12proto/schema.proto\"\xe8\x06\n" +
|
||||
"\x12proto/schema.proto\"\x83\a\n" +
|
||||
"\vVehicleFeed\x120\n" +
|
||||
"\bvehicles\x18\x01 \x03(\v2\x14.VehicleFeed.VehicleR\bvehicles\x12)\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" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\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" +
|
||||
"\x03lat\x18\x01 \x01(\x02R\x03lat\x12\x10\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" +
|
||||
"\x05route\x18\b \x01(\v2\x12.VehicleFeed.RouteR\x05route\x12\x17\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" +
|
||||
"\x10LINE_UNSPECIFIED\x10\x00\x12\t\n" +
|
||||
"\x05GREEN\x10\x01\x12\a\n" +
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "github.com/cjdenio/uta-trax-api/proto";
|
||||
|
||||
message VehicleFeed {
|
||||
@@ -49,6 +50,7 @@ message VehicleFeed {
|
||||
string headsign = 7;
|
||||
Route route = 8;
|
||||
string trip_id = 10;
|
||||
string shape_id = 11;
|
||||
}
|
||||
|
||||
repeated Vehicle vehicles = 1;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.6 MiB |
+1
-1
@@ -300,6 +300,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/js/main.js?v=9"></script>
|
||||
<script type="module" src="/js/main.js?v=14"></script>
|
||||
</body>
|
||||
</html>
|
||||
+50
-12
@@ -13,6 +13,13 @@ function displayLastUpdated() {
|
||||
|
||||
setInterval(displayLastUpdated, 1000)
|
||||
|
||||
const BRT_ROUTES = [
|
||||
"92235", // OGX
|
||||
"3686", // UVX
|
||||
"87711", // MVX
|
||||
"90585", // OGX Wildcat Shuttle
|
||||
]
|
||||
|
||||
const RouteType = {
|
||||
TRAM: 1,
|
||||
RAIL: 3,
|
||||
@@ -91,11 +98,15 @@ const brtLayer = L.layerGroup().addTo(map);
|
||||
const traxLayer = L.layerGroup().addTo(map);
|
||||
const frontRunnerLayer = L.layerGroup().addTo(map);
|
||||
|
||||
L.polyline(shapes.blueLine, { color: "#004a97", zIndexOffset: 10 }).addTo(map);
|
||||
L.polyline(shapes.redLine, { color: "#be2036", zIndexOffset: 10 }).addTo(map);
|
||||
L.polyline(shapes.greenLine, { color: "#2eb566", zIndexOffset: 10 }).addTo(map);
|
||||
L.polyline(shapes.sLine, { color: "#77777a", zIndexOffset: 10 }).addTo(map);
|
||||
L.polyline(shapes.frontRunner, { color: "#c227b9", zIndexOffset: 10 }).addTo(map);
|
||||
const railLayer = L.layerGroup().addTo(map);
|
||||
|
||||
const currentRouteLayer = L.layerGroup().addTo(map);
|
||||
|
||||
L.polyline(shapes.blueLine, { color: "#004a97", zIndexOffset: 10 }).addTo(railLayer);
|
||||
L.polyline(shapes.redLine, { color: "#be2036", zIndexOffset: 10 }).addTo(railLayer);
|
||||
L.polyline(shapes.greenLine, { color: "#2eb566", zIndexOffset: 10 }).addTo(railLayer);
|
||||
L.polyline(shapes.sLine, { color: "#77777a", zIndexOffset: 10 }).addTo(railLayer);
|
||||
L.polyline(shapes.frontRunner, { color: "#c227b9", zIndexOffset: 10 }).addTo(railLayer);
|
||||
|
||||
// render stations
|
||||
for (const station of stations) {
|
||||
@@ -138,6 +149,8 @@ function clearMap() {
|
||||
function routeDesignator(route) {
|
||||
if (route.id == "92235") {
|
||||
return "OGX";
|
||||
} else if (route.id == "90585") {
|
||||
return "OGX Wildcat Shuttle";
|
||||
} else if (route.id == "3686") {
|
||||
return "UVX";
|
||||
} else if (route.id == "87711") {
|
||||
@@ -202,7 +215,7 @@ function renderVehicleIcon(vehicle) {
|
||||
className: "",
|
||||
html: `<div class="vehicle ${
|
||||
vehicle.route.type == RouteType.BUS &&
|
||||
!["92235", "3686", "87711"].includes(vehicle.route.id)
|
||||
!BRT_ROUTES.includes(vehicle.route.id)
|
||||
? "vehicle-plain"
|
||||
: ""
|
||||
} ${
|
||||
@@ -240,6 +253,18 @@ function vehiclePopupContent(vehicle) {
|
||||
return content
|
||||
}
|
||||
|
||||
const tripShapeCache = new Map();
|
||||
|
||||
async function getTripShape(shapeId) {
|
||||
if (tripShapeCache.has(shapeId)) {
|
||||
return tripShapeCache.get(shapeId);
|
||||
} else {
|
||||
const data = await fetch(`/api/shapes/${shapeId}`).then(r => r.json());
|
||||
tripShapeCache.set(shapeId, data.shape);
|
||||
return data.shape;
|
||||
}
|
||||
}
|
||||
|
||||
function renderVehicle(vehicle) {
|
||||
return L.marker([vehicle.lat, vehicle.lon], {
|
||||
zIndexOffset:
|
||||
@@ -248,11 +273,23 @@ function renderVehicle(vehicle) {
|
||||
? 4000
|
||||
: 3000,
|
||||
icon: renderVehicleIcon(vehicle),
|
||||
}).bindPopup(vehiclePopupContent(vehicle));
|
||||
}).bindPopup(vehiclePopupContent(vehicle)).on("popupopen", () => {
|
||||
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);
|
||||
})
|
||||
}).on("popupclose", () => {
|
||||
for (const layer of railLayer.getLayers()) {
|
||||
layer.setStyle({ opacity: 1.0 });
|
||||
}
|
||||
currentRouteLayer.clearLayers();
|
||||
});
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
const { vehicles, info } = await fetch("/api", { headers: { Accept: "application/json" } }).then((r) => r.json());
|
||||
const { vehicles = [], info } = await fetch("/api", { headers: { Accept: "application/json" } }).then((r) => r.json());
|
||||
|
||||
lastUpdated = info.last_update
|
||||
displayLastUpdated()
|
||||
@@ -283,18 +320,19 @@ async function reload() {
|
||||
currentVehicles.get(vehicle.id).marker.setPopupContent(vehiclePopupContent(vehicle))
|
||||
} else {
|
||||
const marker = renderVehicle(vehicle)
|
||||
marker.addTo(
|
||||
["92235", "3686", "87711"].includes(vehicle.route.id)
|
||||
const layer = BRT_ROUTES.includes(vehicle.route.id)
|
||||
? brtLayer
|
||||
: vehicle.route.type == RouteType.TRAM
|
||||
? traxLayer
|
||||
: vehicle.route.type == RouteType.RAIL
|
||||
? frontRunnerLayer
|
||||
: busLayer
|
||||
);
|
||||
|
||||
marker.addTo(layer);
|
||||
currentVehicles.set(vehicle.id, {
|
||||
vehicle,
|
||||
marker,
|
||||
layer,
|
||||
})
|
||||
}
|
||||
});
|
||||
@@ -302,7 +340,7 @@ async function reload() {
|
||||
// remove any vehicles that should be removed
|
||||
for (const key of currentVehicles.keys()) {
|
||||
if (!seenVehicleIds.has(key)) {
|
||||
currentVehicles.get(key).marker.remove()
|
||||
currentVehicles.get(key).marker.removeFrom(currentVehicles.get(key).layer)
|
||||
currentVehicles.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
Reference in new issue
Block a user