This commit is contained in:
2025-12-28 16:58:34 -05:00
parent cff2fa89a8
commit 8b6f755893
8 changed files with 2934 additions and 159 deletions
+60 -73
View File
@@ -73,21 +73,21 @@ func getStationForVehicle(vehicle *pb.VehiclePosition) *pb.VehicleFeed_Station {
return nearestStop
}
func getVehicles() ([]*pb.VehiclePosition, error) {
func getVehicles() ([]*pb.VehiclePosition, *pb.FeedHeader, error) {
resp, err := http.Get("https://apps.rideuta.com/tms/gtfs/Vehicle")
if err != nil {
return nil, err
return nil, nil, err
}
bytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
return nil, nil, err
}
feed := pb.FeedMessage{}
err = proto.Unmarshal(bytes, &feed)
if err != nil {
return nil, err
return nil, nil, err
}
log.Printf("Found %d vehicles...", len(feed.Entity))
@@ -99,7 +99,7 @@ func getVehicles() ([]*pb.VehiclePosition, error) {
}
}
return vehicles, nil
return vehicles, feed.Header, nil
}
type TripInfo struct {
@@ -108,80 +108,72 @@ type TripInfo struct {
Headsign string
}
var trips = make(map[string]*TripInfo)
func loadTrips() error {
rows, err := scheduleDb.Query("SELECT route_id, trip_id, trip_headsign, direction_id FROM trips;")
if err != nil {
return err
}
for {
if next := rows.Next(); next {
trip_info := new(TripInfo)
var route_id string
var trip_id string
var trip_headsign string
var direction_id int32
err = rows.Scan(&route_id, &trip_id, &trip_headsign, &direction_id)
if err != nil {
continue
}
trip_info.Headsign = trip_headsign
trip_info.Direction = direction_id
switch route_id {
case "8246":
trip_info.Line = pb.VehicleFeed_RED
trips[trip_id] = trip_info
case "39020":
trip_info.Line = pb.VehicleFeed_GREEN
trips[trip_id] = trip_info
case "5907":
trip_info.Line = pb.VehicleFeed_BLUE
trips[trip_id] = trip_info
case "45389":
trip_info.Line = pb.VehicleFeed_STREETCAR
trips[trip_id] = trip_info
// case "41065":
// trip_info.Line = pb.VehicleFeed_FRONTRUNNER
// trips[record[trip_id]] = trip_info
}
} else {
break
}
}
return nil
}
func feedifyVehicles(vehicles []*pb.VehiclePosition) pb.VehicleFeed {
func feedifyVehicles(vehicles []*pb.VehiclePosition, header *pb.FeedHeader) pb.VehicleFeed {
vehicle_feed := make([]*pb.VehicleFeed_Vehicle, 0, len(vehicles))
for _, vehicle := range vehicles {
trip, ok := trips[*vehicle.Trip.TripId]
if !ok {
log.Printf("No matching trip '%s', skipping...", *vehicle.Trip.TripId)
rows, err := scheduleDb.Query(`
SELECT
trips.route_id,
trips.trip_headsign,
routes.route_type,
routes.route_color,
routes.route_short_name,
routes.route_long_name
FROM trips
INNER JOIN routes ON routes.route_id = trips.route_id
WHERE trips.trip_id = ?
LIMIT 1;
`, *vehicle.Trip.TripId)
if err != nil {
fmt.Println(err)
continue
}
var route_id string
var trip_headsign sql.NullString
var route_type int32
var route_color sql.NullString
var route_short_name sql.NullString
var route_long_name sql.NullString
if !rows.Next() {
fmt.Println(rows.Err())
continue
}
err = rows.Scan(&route_id, &trip_headsign, &route_type, &route_color, &route_short_name, &route_long_name)
if err != nil {
fmt.Println(err)
continue
}
rows.Close()
vehicle_feed = append(vehicle_feed, &pb.VehicleFeed_Vehicle{
Lat: *vehicle.Position.Latitude,
Lon: *vehicle.Position.Longitude,
Line: trip.Line,
Id: *vehicle.Vehicle.Id,
Direction: trip.Direction,
Lat: *vehicle.Position.Latitude,
Lon: *vehicle.Position.Longitude,
Bearing: *vehicle.Position.Bearing,
// Line: trip.Line,
Id: *vehicle.Vehicle.Id,
// Direction: trip.Direction,
NearestStation: getStationForVehicle(vehicle),
Headsign: trip.Headsign,
Headsign: trip_headsign.String,
Route: &pb.VehicleFeed_Route{
Id: route_id,
Type: pb.VehicleFeed_Route_RouteType(route_type + 1), // lol
Color: route_color.String,
ShortName: route_short_name.String,
LongName: route_long_name.String,
},
})
}
return pb.VehicleFeed{
Vehicles: vehicle_feed,
Info: &pb.VehicleFeed_FeedInfo{
LastUpdate: *header.Timestamp,
},
}
}
@@ -193,12 +185,7 @@ func main() {
log.Fatal(err)
}
fmt.Println("Loading trip data...")
scheduleDb = _db
if err := loadTrips(); err != nil {
log.Fatalln(err)
}
// vehicles, err := getVehicles()
// if err != nil {
@@ -217,14 +204,14 @@ func main() {
})
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
vehicles, err := getVehicles()
vehicles, header, err := getVehicles()
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
feed := feedifyVehicles(vehicles)
feed := feedifyVehicles(vehicles, header)
b, err := proto.Marshal(&feed)
if err != nil {
log.Println(err)
+252 -27
View File
@@ -79,9 +79,65 @@ func (VehicleFeed_Line) EnumDescriptor() ([]byte, []int) {
return file_proto_schema_proto_rawDescGZIP(), []int{0, 0}
}
type VehicleFeed_Route_RouteType int32
const (
VehicleFeed_Route_UNSPECIFIED VehicleFeed_Route_RouteType = 0
VehicleFeed_Route_TRAM VehicleFeed_Route_RouteType = 1
VehicleFeed_Route_SUBWAY VehicleFeed_Route_RouteType = 2
VehicleFeed_Route_RAIL VehicleFeed_Route_RouteType = 3
VehicleFeed_Route_BUS VehicleFeed_Route_RouteType = 4
)
// Enum value maps for VehicleFeed_Route_RouteType.
var (
VehicleFeed_Route_RouteType_name = map[int32]string{
0: "UNSPECIFIED",
1: "TRAM",
2: "SUBWAY",
3: "RAIL",
4: "BUS",
}
VehicleFeed_Route_RouteType_value = map[string]int32{
"UNSPECIFIED": 0,
"TRAM": 1,
"SUBWAY": 2,
"RAIL": 3,
"BUS": 4,
}
)
func (x VehicleFeed_Route_RouteType) Enum() *VehicleFeed_Route_RouteType {
p := new(VehicleFeed_Route_RouteType)
*p = x
return p
}
func (x VehicleFeed_Route_RouteType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (VehicleFeed_Route_RouteType) Descriptor() protoreflect.EnumDescriptor {
return file_proto_schema_proto_enumTypes[1].Descriptor()
}
func (VehicleFeed_Route_RouteType) Type() protoreflect.EnumType {
return &file_proto_schema_proto_enumTypes[1]
}
func (x VehicleFeed_Route_RouteType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use VehicleFeed_Route_RouteType.Descriptor instead.
func (VehicleFeed_Route_RouteType) EnumDescriptor() ([]byte, []int) {
return file_proto_schema_proto_rawDescGZIP(), []int{0, 1, 0}
}
type VehicleFeed struct {
state protoimpl.MessageState `protogen:"open.v1"`
Vehicles []*VehicleFeed_Vehicle `protobuf:"bytes,1,rep,name=vehicles,proto3" json:"vehicles,omitempty"`
Info *VehicleFeed_FeedInfo `protobuf:"bytes,2,opt,name=info,proto3" json:"info,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -123,6 +179,133 @@ func (x *VehicleFeed) GetVehicles() []*VehicleFeed_Vehicle {
return nil
}
func (x *VehicleFeed) GetInfo() *VehicleFeed_FeedInfo {
if x != nil {
return x.Info
}
return nil
}
type VehicleFeed_FeedInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
LastUpdate uint64 `protobuf:"varint,1,opt,name=last_update,json=lastUpdate,proto3" json:"last_update,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *VehicleFeed_FeedInfo) Reset() {
*x = VehicleFeed_FeedInfo{}
mi := &file_proto_schema_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *VehicleFeed_FeedInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*VehicleFeed_FeedInfo) ProtoMessage() {}
func (x *VehicleFeed_FeedInfo) ProtoReflect() protoreflect.Message {
mi := &file_proto_schema_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use VehicleFeed_FeedInfo.ProtoReflect.Descriptor instead.
func (*VehicleFeed_FeedInfo) Descriptor() ([]byte, []int) {
return file_proto_schema_proto_rawDescGZIP(), []int{0, 0}
}
func (x *VehicleFeed_FeedInfo) GetLastUpdate() uint64 {
if x != nil {
return x.LastUpdate
}
return 0
}
type VehicleFeed_Route struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type VehicleFeed_Route_RouteType `protobuf:"varint,1,opt,name=type,proto3,enum=VehicleFeed_Route_RouteType" json:"type,omitempty"`
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
Color string `protobuf:"bytes,3,opt,name=color,proto3" json:"color,omitempty"`
ShortName string `protobuf:"bytes,4,opt,name=short_name,json=shortName,proto3" json:"short_name,omitempty"`
LongName string `protobuf:"bytes,5,opt,name=long_name,json=longName,proto3" json:"long_name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *VehicleFeed_Route) Reset() {
*x = VehicleFeed_Route{}
mi := &file_proto_schema_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *VehicleFeed_Route) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*VehicleFeed_Route) ProtoMessage() {}
func (x *VehicleFeed_Route) ProtoReflect() protoreflect.Message {
mi := &file_proto_schema_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use VehicleFeed_Route.ProtoReflect.Descriptor instead.
func (*VehicleFeed_Route) Descriptor() ([]byte, []int) {
return file_proto_schema_proto_rawDescGZIP(), []int{0, 1}
}
func (x *VehicleFeed_Route) GetType() VehicleFeed_Route_RouteType {
if x != nil {
return x.Type
}
return VehicleFeed_Route_UNSPECIFIED
}
func (x *VehicleFeed_Route) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *VehicleFeed_Route) GetColor() string {
if x != nil {
return x.Color
}
return ""
}
func (x *VehicleFeed_Route) GetShortName() string {
if x != nil {
return x.ShortName
}
return ""
}
func (x *VehicleFeed_Route) GetLongName() string {
if x != nil {
return x.LongName
}
return ""
}
type VehicleFeed_Station struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
@@ -135,7 +318,7 @@ type VehicleFeed_Station struct {
func (x *VehicleFeed_Station) Reset() {
*x = VehicleFeed_Station{}
mi := &file_proto_schema_proto_msgTypes[1]
mi := &file_proto_schema_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -147,7 +330,7 @@ func (x *VehicleFeed_Station) String() string {
func (*VehicleFeed_Station) ProtoMessage() {}
func (x *VehicleFeed_Station) ProtoReflect() protoreflect.Message {
mi := &file_proto_schema_proto_msgTypes[1]
mi := &file_proto_schema_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -160,7 +343,7 @@ func (x *VehicleFeed_Station) ProtoReflect() protoreflect.Message {
// Deprecated: Use VehicleFeed_Station.ProtoReflect.Descriptor instead.
func (*VehicleFeed_Station) Descriptor() ([]byte, []int) {
return file_proto_schema_proto_rawDescGZIP(), []int{0, 0}
return file_proto_schema_proto_rawDescGZIP(), []int{0, 2}
}
func (x *VehicleFeed_Station) GetId() string {
@@ -195,18 +378,20 @@ type VehicleFeed_Vehicle struct {
state protoimpl.MessageState `protogen:"open.v1"`
Lat float32 `protobuf:"fixed32,1,opt,name=lat,proto3" json:"lat,omitempty"`
Lon float32 `protobuf:"fixed32,2,opt,name=lon,proto3" json:"lon,omitempty"`
Bearing float32 `protobuf:"fixed32,9,opt,name=bearing,proto3" json:"bearing,omitempty"`
Line VehicleFeed_Line `protobuf:"varint,3,opt,name=line,proto3,enum=VehicleFeed_Line" json:"line,omitempty"`
Direction int32 `protobuf:"varint,4,opt,name=direction,proto3" json:"direction,omitempty"`
Id string `protobuf:"bytes,5,opt,name=id,proto3" json:"id,omitempty"`
NearestStation *VehicleFeed_Station `protobuf:"bytes,6,opt,name=nearest_station,json=nearestStation,proto3" json:"nearest_station,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"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *VehicleFeed_Vehicle) Reset() {
*x = VehicleFeed_Vehicle{}
mi := &file_proto_schema_proto_msgTypes[2]
mi := &file_proto_schema_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -218,7 +403,7 @@ func (x *VehicleFeed_Vehicle) String() string {
func (*VehicleFeed_Vehicle) ProtoMessage() {}
func (x *VehicleFeed_Vehicle) ProtoReflect() protoreflect.Message {
mi := &file_proto_schema_proto_msgTypes[2]
mi := &file_proto_schema_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -231,7 +416,7 @@ func (x *VehicleFeed_Vehicle) ProtoReflect() protoreflect.Message {
// Deprecated: Use VehicleFeed_Vehicle.ProtoReflect.Descriptor instead.
func (*VehicleFeed_Vehicle) Descriptor() ([]byte, []int) {
return file_proto_schema_proto_rawDescGZIP(), []int{0, 1}
return file_proto_schema_proto_rawDescGZIP(), []int{0, 3}
}
func (x *VehicleFeed_Vehicle) GetLat() float32 {
@@ -248,6 +433,13 @@ func (x *VehicleFeed_Vehicle) GetLon() float32 {
return 0
}
func (x *VehicleFeed_Vehicle) GetBearing() float32 {
if x != nil {
return x.Bearing
}
return 0
}
func (x *VehicleFeed_Vehicle) GetLine() VehicleFeed_Line {
if x != nil {
return x.Line
@@ -283,26 +475,53 @@ func (x *VehicleFeed_Vehicle) GetHeadsign() string {
return ""
}
func (x *VehicleFeed_Vehicle) GetRoute() *VehicleFeed_Route {
if x != nil {
return x.Route
}
return nil
}
var File_proto_schema_proto protoreflect.FileDescriptor
const file_proto_schema_proto_rawDesc = "" +
"\n" +
"\x12proto/schema.proto\"\xce\x03\n" +
"\x12proto/schema.proto\"\xcf\x06\n" +
"\vVehicleFeed\x120\n" +
"\bvehicles\x18\x01 \x03(\v2\x14.VehicleFeed.VehicleR\bvehicles\x1aQ\n" +
"\bvehicles\x18\x01 \x03(\v2\x14.VehicleFeed.VehicleR\bvehicles\x12)\n" +
"\x04info\x18\x02 \x01(\v2\x15.VehicleFeed.FeedInfoR\x04info\x1a+\n" +
"\bFeedInfo\x12\x1f\n" +
"\vlast_update\x18\x01 \x01(\x04R\n" +
"lastUpdate\x1a\xe2\x01\n" +
"\x05Route\x120\n" +
"\x04type\x18\x01 \x01(\x0e2\x1c.VehicleFeed.Route.RouteTypeR\x04type\x12\x0e\n" +
"\x02id\x18\x02 \x01(\tR\x02id\x12\x14\n" +
"\x05color\x18\x03 \x01(\tR\x05color\x12\x1d\n" +
"\n" +
"short_name\x18\x04 \x01(\tR\tshortName\x12\x1b\n" +
"\tlong_name\x18\x05 \x01(\tR\blongName\"E\n" +
"\tRouteType\x12\x0f\n" +
"\vUNSPECIFIED\x10\x00\x12\b\n" +
"\x04TRAM\x10\x01\x12\n" +
"\n" +
"\x06SUBWAY\x10\x02\x12\b\n" +
"\x04RAIL\x10\x03\x12\a\n" +
"\x03BUS\x10\x04\x1aQ\n" +
"\aStation\x12\x0e\n" +
"\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\xdd\x01\n" +
"\x03lon\x18\x04 \x01(\x02R\x03lon\x1a\xa1\x02\n" +
"\aVehicle\x12\x10\n" +
"\x03lat\x18\x01 \x01(\x02R\x03lat\x12\x10\n" +
"\x03lon\x18\x02 \x01(\x02R\x03lon\x12%\n" +
"\x03lon\x18\x02 \x01(\x02R\x03lon\x12\x18\n" +
"\abearing\x18\t \x01(\x02R\abearing\x12%\n" +
"\x04line\x18\x03 \x01(\x0e2\x11.VehicleFeed.LineR\x04line\x12\x1c\n" +
"\tdirection\x18\x04 \x01(\x05R\tdirection\x12\x0e\n" +
"\x02id\x18\x05 \x01(\tR\x02id\x12=\n" +
"\x0fnearest_station\x18\x06 \x01(\v2\x14.VehicleFeed.StationR\x0enearestStation\x12\x1a\n" +
"\bheadsign\x18\a \x01(\tR\bheadsign\"Z\n" +
"\bheadsign\x18\a \x01(\tR\bheadsign\x12(\n" +
"\x05route\x18\b \x01(\v2\x12.VehicleFeed.RouteR\x05route\"Z\n" +
"\x04Line\x12\x14\n" +
"\x10LINE_UNSPECIFIED\x10\x00\x12\t\n" +
"\x05GREEN\x10\x01\x12\a\n" +
@@ -323,23 +542,29 @@ func file_proto_schema_proto_rawDescGZIP() []byte {
return file_proto_schema_proto_rawDescData
}
var file_proto_schema_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_proto_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_proto_schema_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_proto_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_proto_schema_proto_goTypes = []any{
(VehicleFeed_Line)(0), // 0: VehicleFeed.Line
(*VehicleFeed)(nil), // 1: VehicleFeed
(*VehicleFeed_Station)(nil), // 2: VehicleFeed.Station
(*VehicleFeed_Vehicle)(nil), // 3: VehicleFeed.Vehicle
(VehicleFeed_Line)(0), // 0: VehicleFeed.Line
(VehicleFeed_Route_RouteType)(0), // 1: VehicleFeed.Route.RouteType
(*VehicleFeed)(nil), // 2: VehicleFeed
(*VehicleFeed_FeedInfo)(nil), // 3: VehicleFeed.FeedInfo
(*VehicleFeed_Route)(nil), // 4: VehicleFeed.Route
(*VehicleFeed_Station)(nil), // 5: VehicleFeed.Station
(*VehicleFeed_Vehicle)(nil), // 6: VehicleFeed.Vehicle
}
var file_proto_schema_proto_depIdxs = []int32{
3, // 0: VehicleFeed.vehicles:type_name -> VehicleFeed.Vehicle
0, // 1: VehicleFeed.Vehicle.line:type_name -> VehicleFeed.Line
2, // 2: VehicleFeed.Vehicle.nearest_station:type_name -> VehicleFeed.Station
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
6, // 0: VehicleFeed.vehicles:type_name -> VehicleFeed.Vehicle
3, // 1: VehicleFeed.info:type_name -> VehicleFeed.FeedInfo
1, // 2: VehicleFeed.Route.type:type_name -> VehicleFeed.Route.RouteType
0, // 3: VehicleFeed.Vehicle.line:type_name -> VehicleFeed.Line
5, // 4: VehicleFeed.Vehicle.nearest_station:type_name -> VehicleFeed.Station
4, // 5: VehicleFeed.Vehicle.route:type_name -> VehicleFeed.Route
6, // [6:6] is the sub-list for method output_type
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
}
func init() { file_proto_schema_proto_init() }
@@ -352,8 +577,8 @@ func file_proto_schema_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_schema_proto_rawDesc), len(file_proto_schema_proto_rawDesc)),
NumEnums: 1,
NumMessages: 3,
NumEnums: 2,
NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},
+23
View File
@@ -11,6 +11,26 @@ message VehicleFeed {
FRONTRUNNER = 5;
}
message FeedInfo {
uint64 last_update = 1;
}
message Route {
enum RouteType {
UNSPECIFIED = 0;
TRAM = 1;
SUBWAY = 2;
RAIL = 3;
BUS = 4;
}
RouteType type = 1;
string id = 2;
string color = 3;
string short_name = 4;
string long_name = 5;
}
message Station {
string id = 1;
string name = 2;
@@ -21,12 +41,15 @@ message VehicleFeed {
message Vehicle {
float lat = 1;
float lon = 2;
float bearing = 9;
Line line = 3;
int32 direction = 4;
string id = 5;
Station nearest_station = 6;
string headsign = 7;
Route route = 8;
}
repeated Vehicle vehicles = 1;
FeedInfo info = 2;
}
+96 -59
View File
@@ -1,3 +1,5 @@
<!-- i SWEAR i write good code sometimes -->
<!DOCTYPE html>
<html>
@@ -14,73 +16,108 @@
body {
margin: 0;
}
#sidebar {
flex-basis: 300px;
flex-shrink: 0;
padding: 10px;
}
.vehicle {
border-radius: 999px;
width: 20px;
height: 20px;
margin-left: -10px;
margin-top: -10px;
padding: 8px;
fill: white;
/* box-shadow: 0px 0px 10px black; */
border: 1px solid black;
position: relative;
background-color: var(--color);
}
.vehicle-plain {
/* bus */
background-color: white;
border-color: var(--color);
fill: var(--color);
}
.vehicle-small {
width: 15px;
height: 15px;
margin-left: -7px;
margin-top: -7px;
}
</style>
</head>
<body>
<div id="map" style="height: 100vh"></div>
<div style="display: flex">
<div id="map" style="height: 100vh; flex-grow: 1"></div>
<div id="sidebar">
<h1>Every Transit Vehicle in Salt Lake City*</h1>
<small>*with a tracker</small>
<script src="//cdn.jsdelivr.net/npm/protobufjs@7.X.X/dist/protobuf.min.js"></script>
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""
></script>
<hr />
<script>
const COLORS = {
1: "#2eb566",
2: "#be2036",
3: "#004a97",
4: "#77777a",
5: "#c227b9",
};
let markers = []
<div>
<label for="bus">
Regular Bus
<input
type="checkbox"
name="bus"
id="bus"
checked
class="layer-toggle"
/>
</label>
</div>
<div>
<label for="brt">
BRT
<input
type="checkbox"
name="brt"
id="brt"
checked
class="layer-toggle"
/>
</label>
</div>
<div>
<label for="trax">
TRAX + S-Line (light rail)
<input
type="checkbox"
name="trax"
id="trax"
checked
class="layer-toggle"
/>
</label>
</div>
<div>
<label for="frontrunner">
FrontRunner (commuter rail)
<input
type="checkbox"
name="frontrunner"
id="frontrunner"
checked
class="layer-toggle"
/>
</label>
</div>
var map = L.map("map").setView([40.656734, -111.890818], 12);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map);
<hr>
function clearMap() {
for (const marker of markers) {
marker.remove();
}
<div>
last updated: <span id="last-updated">-</span>
</div>
</div>
</div>
markers = [];
}
protobuf.load("/schema.proto").then((root) => {
async function reload() {
const bin = await fetch("/api").then((r) => r.arrayBuffer());
const { vehicles } = root
.lookupType("VehicleFeed")
.decode(new Uint8Array(bin));
clearMap();
vehicles.forEach((vehicle) => {
markers.push(
L.circleMarker([vehicle.lat, vehicle.lon], {
fillColor: COLORS[vehicle.line],
color: COLORS[vehicle.line],
fillOpacity: 0.5,
radius: 8,
})
.addTo(map)
.bindPopup(
`${vehicle.headsign}<br />@ ${vehicle.nearestStation.name}`
)
);
});
}
reload();
setInterval(reload, 5000);
});
</script>
<script type="module" src="/js/main.js"></script>
</body>
</html>
+156
View File
@@ -0,0 +1,156 @@
import * as shapes from "./shapes.js";
import protobuf from "https://cdn.jsdelivr.net/npm/protobufjs@8.0.0/dist/protobuf.js/+esm";
import * as L from "https://unpkg.com/leaflet@1.9.4/dist/leaflet-src.esm.js";
let lastUpdated;
function displayLastUpdated() {
document.getElementById("last-updated").innerText = `${Math.floor((Date.now() - (lastUpdated * 1000)) / 1000)}s ago`
console.log("yeah")
}
setInterval(displayLastUpdated, 1000)
const ICONS = {
bus: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M192 64C139 64 96 107 96 160L96 448C96 477.8 116.4 502.9 144 510L144 544C144 561.7 158.3 576 176 576L192 576C209.7 576 224 561.7 224 544L224 512L416 512L416 544C416 561.7 430.3 576 448 576L464 576C481.7 576 496 561.7 496 544L496 510C523.6 502.9 544 477.8 544 448L544 160C544 107 501 64 448 64L192 64zM160 240C160 222.3 174.3 208 192 208L296 208L296 320L192 320C174.3 320 160 305.7 160 288L160 240zM344 320L344 208L448 208C465.7 208 480 222.3 480 240L480 288C480 305.7 465.7 320 448 320L344 320zM192 384C209.7 384 224 398.3 224 416C224 433.7 209.7 448 192 448C174.3 448 160 433.7 160 416C160 398.3 174.3 384 192 384zM448 384C465.7 384 480 398.3 480 416C480 433.7 465.7 448 448 448C430.3 448 416 433.7 416 416C416 398.3 430.3 384 448 384zM248 136C248 122.7 258.7 112 272 112L368 112C381.3 112 392 122.7 392 136C392 149.3 381.3 160 368 160L272 160C258.7 160 248 149.3 248 136z"/></svg>`,
tram: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 72C128 58.7 138.7 48 152 48L488 48C501.3 48 512 58.7 512 72L512 104C512 117.3 501.3 128 488 128C474.7 128 464 117.3 464 104L464 96L344 96L344 160L384 160C437 160 480 203 480 256L480 416C480 447.2 465.1 475 442 492.5L506.3 568.5C514.9 578.6 513.6 593.8 503.5 602.3C493.4 610.8 478.2 609.6 469.7 599.5L395.1 511.4C391.5 511.8 387.8 512 384 512L256 512C252.2 512 248.5 511.8 244.9 511.4L170.3 599.5C161.7 609.6 146.6 610.9 136.5 602.3C126.4 593.7 125.1 578.6 133.7 568.5L198 492.5C174.9 475 160 447.2 160 416L160 256C160 203 203 160 256 160L296 160L296 96L176 96L176 104C176 117.3 165.3 128 152 128C138.7 128 128 117.3 128 104L128 72zM256 224C238.3 224 224 238.3 224 256L224 288C224 305.7 238.3 320 256 320L384 320C401.7 320 416 305.7 416 288L416 256C416 238.3 401.7 224 384 224L256 224zM288 416C288 398.3 273.7 384 256 384C238.3 384 224 398.3 224 416C224 433.7 238.3 448 256 448C273.7 448 288 433.7 288 416zM384 448C401.7 448 416 433.7 416 416C416 398.3 401.7 384 384 384C366.3 384 352 398.3 352 416C352 433.7 366.3 448 384 448z"/></svg>`,
train: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 160C128 107 171 64 224 64L416 64C469 64 512 107 512 160L512 416C512 456.1 487.4 490.5 452.5 504.8L506.4 568.5C515 578.6 513.7 593.8 503.6 602.3C493.5 610.8 478.3 609.6 469.8 599.5L395.8 512L244.5 512L170.5 599.5C161.9 609.6 146.8 610.9 136.7 602.3C126.6 593.7 125.3 578.6 133.9 568.5L187.8 504.8C152.6 490.5 128 456.1 128 416L128 160zM192 192L192 288C192 305.7 206.3 320 224 320L416 320C433.7 320 448 305.7 448 288L448 192C448 174.3 433.7 160 416 160L224 160C206.3 160 192 174.3 192 192zM320 448C337.7 448 352 433.7 352 416C352 398.3 337.7 384 320 384C302.3 384 288 398.3 288 416C288 433.7 302.3 448 320 448z"/></svg>`,
};
var map = L.map("map").setView([40.656734, -111.890818], 12);
L.tileLayer(
"https://api.mapbox.com/styles/v1/mapbox/light-v11/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1IjoiY2pkZW5pbyIsImEiOiJjbHdiMG52amcwaGd4MmttbWtlOWt5Mm1iIn0.GhHTt4W_mZpQcLYNkhsG_w",
{
maxZoom: 19,
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}
).addTo(map);
const busLayer = L.layerGroup().addTo(map);
const brtLayer = L.layerGroup().addTo(map);
const traxLayer = L.layerGroup().addTo(map);
const frontRunnerLayer = L.layerGroup().addTo(map);
L.polyline(shapes.blueLine, { color: "#004a97" }).addTo(map);
L.polyline(shapes.redLine, { color: "#be2036" }).addTo(map);
L.polyline(shapes.greenLine, { color: "#2eb566" }).addTo(map);
L.polyline(shapes.sLine, { color: "#77777a" }).addTo(map);
L.polyline(shapes.frontRunner, { color: "#c227b9" }).addTo(map);
function clearMap() {
busLayer.clearLayers();
brtLayer.clearLayers();
traxLayer.clearLayers();
frontRunnerLayer.clearLayers();
}
protobuf.load("/schema.proto").then((root) => {
const RouteType = root.VehicleFeed.Route.RouteType;
function routeDesignator(route) {
if (route.id == "92235") {
return "OGX";
} else if (route.id == "3686") {
return "UVX";
} else if (route.type == RouteType.BUS) {
return "#" + route.shortName;
} else {
return route.longName;
}
}
async function reload() {
const bin = await fetch("/api").then((r) => r.arrayBuffer());
const { vehicles, info } = root
.lookupType("VehicleFeed")
.decode(new Uint8Array(bin));
clearMap();
lastUpdated = info.lastUpdate
displayLastUpdated()
vehicles.forEach((vehicle) => {
L.marker([vehicle.lat, vehicle.lon], {
zIndexOffset:
vehicle.route.type == RouteType.TRAM ||
vehicle.route.type == RouteType.RAIL
? 1000
: 1,
icon: L.divIcon({
className: "",
html: `<div class="vehicle ${
vehicle.route.type == RouteType.BUS &&
!["92235", "3686"].includes(vehicle.route.id)
? "vehicle-plain"
: ""
} ${
vehicle.route.type == RouteType.BUS ? "vehicle-small" : ""
}" style="--color: #${vehicle.route.color};">
<div style="position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;">
<svg style="transform: rotate(${
vehicle.bearing
}deg) translateY(-12px); width: 18px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M300.3 199.2C312.9 188.9 331.4 189.7 343.1 201.4L471.1 329.4C480.3 338.6 483 352.3 478 364.3C473 376.3 461.4 384 448.5 384L192.5 384C179.6 384 167.9 376.2 162.9 364.2C157.9 352.2 160.7 338.5 169.9 329.4L297.9 201.4L300.3 199.2z"/></svg>
</div>
${
vehicle.route.type == RouteType.BUS
? ICONS.bus
: vehicle.route.type == RouteType.TRAM
? ICONS.tram
: vehicle.route.type == RouteType.RAIL
? ICONS.train
: ""
}
</div>`,
}),
})
.addTo(
["92235", "3686"].includes(vehicle.route.id)
? brtLayer
: vehicle.route.type == RouteType.TRAM
? traxLayer
: vehicle.route.type == RouteType.RAIL
? frontRunnerLayer
: busLayer
)
.bindPopup(
`${routeDesignator(vehicle.route)} to ${vehicle.headsign.replace(
/^to /i,
""
)}<br />@ ${vehicle.nearestStation?.name}`
);
});
}
reload();
setInterval(reload, 5000);
for (const checkbox of document.querySelectorAll(".layer-toggle")) {
checkbox.addEventListener("input", (e) => {
let layer;
switch (e.target.name) {
case "bus":
layer = busLayer;
break;
case "brt":
layer = brtLayer;
break;
case "trax":
layer = traxLayer;
break;
case "frontrunner":
layer = frontRunnerLayer;
break;
}
if (e.target.checked) {
layer?.addTo(map);
} else {
layer?.remove();
}
});
}
});
+2341
View File
File diff suppressed because it is too large Load Diff
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -eo pipefail
curl --output gtfs.zip --location https://gtfsfeed.rideuta.com/GTFS.zip
gtfs-import --gtfsPath gtfs.zip --sqlitePath uta-gtfs.db
BIN
View File
Binary file not shown.