mirror of
https://github.com/cjdenio/uta-trax-api.git
synced 2026-10-03 16:06:19 +00:00
Compare commits
6
Commits
0831e523fa
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03ded9250d | ||
|
|
74eb8e55e3 | ||
|
|
f84c2143b8 | ||
|
|
b0c522d994 | ||
|
|
ca0939ee67 | ||
|
|
b7321adbe4 |
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`
|
||||||
|
|
||||||
|

|
||||||
+69
-2
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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() {
|
func main() {
|
||||||
fmt.Println("Opening database...")
|
fmt.Println("Opening database...")
|
||||||
|
|
||||||
@@ -275,6 +340,8 @@ 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/shapes/{shape}", shapeHandler)
|
||||||
|
|
||||||
port := "3000"
|
port := "3000"
|
||||||
if portEnv, ok := os.LookupEnv("PORT"); ok {
|
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"`
|
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" +
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.6 MiB |
+1
-1
@@ -300,6 +300,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/js/main.js?v=10"></script>
|
<script type="module" src="/js/main.js?v=14"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+45
-8
@@ -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,
|
||||||
@@ -91,11 +98,15 @@ const brtLayer = L.layerGroup().addTo(map);
|
|||||||
const traxLayer = L.layerGroup().addTo(map);
|
const traxLayer = L.layerGroup().addTo(map);
|
||||||
const frontRunnerLayer = L.layerGroup().addTo(map);
|
const frontRunnerLayer = L.layerGroup().addTo(map);
|
||||||
|
|
||||||
L.polyline(shapes.blueLine, { color: "#004a97", zIndexOffset: 10 }).addTo(map);
|
const railLayer = L.layerGroup().addTo(map);
|
||||||
L.polyline(shapes.redLine, { color: "#be2036", zIndexOffset: 10 }).addTo(map);
|
|
||||||
L.polyline(shapes.greenLine, { color: "#2eb566", zIndexOffset: 10 }).addTo(map);
|
const currentRouteLayer = L.layerGroup().addTo(map);
|
||||||
L.polyline(shapes.sLine, { color: "#77777a", zIndexOffset: 10 }).addTo(map);
|
|
||||||
L.polyline(shapes.frontRunner, { color: "#c227b9", zIndexOffset: 10 }).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
|
// render stations
|
||||||
for (const station of stations) {
|
for (const station of stations) {
|
||||||
@@ -138,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") {
|
||||||
@@ -202,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"
|
||||||
: ""
|
: ""
|
||||||
} ${
|
} ${
|
||||||
@@ -240,6 +253,18 @@ function vehiclePopupContent(vehicle) {
|
|||||||
return content
|
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) {
|
function renderVehicle(vehicle) {
|
||||||
return L.marker([vehicle.lat, vehicle.lon], {
|
return L.marker([vehicle.lat, vehicle.lon], {
|
||||||
zIndexOffset:
|
zIndexOffset:
|
||||||
@@ -248,7 +273,19 @@ function renderVehicle(vehicle) {
|
|||||||
? 4000
|
? 4000
|
||||||
: 3000,
|
: 3000,
|
||||||
icon: renderVehicleIcon(vehicle),
|
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() {
|
async function reload() {
|
||||||
@@ -283,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
|
||||||
|
|||||||
Reference in new issue
Block a user