From 3e39c5e5c041c69de982df5b6f45e95f7f257e3a Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 26 Jul 2024 12:47:08 +0200 Subject: [PATCH] Back to official API --- api/api.go | 2 +- api/destinations.go | 79 +------------- api/lines.go | 37 +++---- api/schedules.go | 256 +++++++++++++++++++++++--------------------- api/stations.go | 133 ++++++++++------------- api/traffic.go | 2 +- 6 files changed, 205 insertions(+), 304 deletions(-) diff --git a/api/api.go b/api/api.go index 90d09ea..b18cb2a 100644 --- a/api/api.go +++ b/api/api.go @@ -8,7 +8,7 @@ import ( "github.com/gin-gonic/gin" ) -const IDFM2_BASEURL = "https://prim.iledefrance-mobilites.fr/marketplace" +const IDFM_BASEURL = "https://prim.iledefrance-mobilites.fr/marketplace" var IDFM_TOKEN = "" diff --git a/api/destinations.go b/api/destinations.go index a40673d..5ba1e5c 100644 --- a/api/destinations.go +++ b/api/destinations.go @@ -1,10 +1,7 @@ package api import ( - "encoding/json" - "fmt" "net/http" - "net/url" "github.com/gin-gonic/gin" ) @@ -63,85 +60,13 @@ type PGDestination struct { Way string `json:"way"` } -func getSchedules(code string) (*IDFMSchedule, error) { - rurl, err := url.JoinPath(IDFM_BASEURL, "lines", code, "schedules") - if err != nil { - return nil, err - } - - requrl, err := url.Parse(rurl) - if err != nil { - return nil, err - } - - reqquery := url.Values{} - reqquery.Add("complete", "false") - requrl.RawQuery = reqquery.Encode() - - req, err := http.NewRequest("GET", requrl.String(), nil) - if err != nil { - return nil, err - } - - req.Header.Add("Accept", "application/json") - req.Header.Add("apikey", IDFM_TOKEN) - - res, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode >= 400 { - return nil, fmt.Errorf("Schedule not found") - } - - var schedules IDFMSchedule - - dec := json.NewDecoder(res.Body) - if err = dec.Decode(&schedules); err != nil { - return nil, err - } - - return &schedules, nil -} - func getDestinations(code string) ([]PGDestination, error) { - schedule, err := getSchedules(code) - if err != nil { - return nil, err - } - - var pgd []PGDestination - -destination: - for i, s := range schedule.Schedules { - for _, d := range pgd { - if d.Name == s.To { - continue destination - } - } - - way := "R" - if i%2 == 0 { - way = "A" - } - - pgd = append(pgd, PGDestination{ - Name: s.To, - Way: way, - }) - } - - return pgd, nil + return nil, nil } func declareDestinationsRoutes(router *gin.RouterGroup) { router.GET("/destinations/:type/:code", func(c *gin.Context) { - t := convertLineType(string(c.Param("type"))) - code := convertCode(t, string(c.Param("code"))) - - pgd, err := getDestinations(code) + pgd, err := getDestinations(string(c.Param("code"))) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return diff --git a/api/lines.go b/api/lines.go index 3affcd1..36b3100 100644 --- a/api/lines.go +++ b/api/lines.go @@ -97,29 +97,18 @@ func convertLineType(old string) string { } } -func convertCode(t, code string) string { - if !strings.HasPrefix(code, "line:IDFM:") { - if t == "tram" && !strings.HasPrefix(code, "T") { - code = "T" + code - } - if t == "noctilien" && !strings.HasPrefix(code, "N") { - code = "N" + code - } - - if len(code) != 6 || !strings.HasPrefix(code, "C") { - code = searchLine(t, code) - } - - code = "line:IDFM:" + code - } - - return code -} - func searchLine(t, code string) string { - for _, line := range IDFMLines { - if line.Fields.ShortName == code && (line.Fields.TransportMode == t || strings.ToLower(line.Fields.NetworkName) == t) { - return line.Fields.IdLine + if t == "rer" { + for _, line := range IDFMLines { + if strings.HasSuffix(line.Fields.ShortName, code) && (line.Fields.TransportMode == t || strings.ToLower(line.Fields.NetworkName) == t) { + return line.Fields.IdLine + } + } + } else { + for _, line := range IDFMLines { + if line.Fields.ShortName == code && (line.Fields.TransportMode == t || strings.ToLower(line.Fields.NetworkName) == t) { + return line.Fields.IdLine + } } } @@ -165,10 +154,10 @@ func declareLinesRoutes(router *gin.RouterGroup) { } pgline := PGLine{ - Code: line.Fields.ShortName, + Code: fmt.Sprintf("STIF:Line::%s:", line.Fields.IdLine), Name: name, Directions: "", - Id: fmt.Sprintf("STIF:Line::%s:", line.Fields.IdLine), + Id: line.Fields.IdLine, } lines = append(lines, pgline) diff --git a/api/schedules.go b/api/schedules.go index 28ba970..f743a96 100644 --- a/api/schedules.go +++ b/api/schedules.go @@ -5,9 +5,9 @@ import ( "fmt" "io" "log" + "math" "net/http" "net/url" - "sort" "strconv" "strings" "time" @@ -15,80 +15,79 @@ import ( "github.com/gin-gonic/gin" ) -const IDFM_BASEURL = "https://api-iv.iledefrance-mobilites.fr/" - -type IDFMRealTimeData struct { - LineId string `json:"lineId"` - ShortName string `json:"shortName"` - VehicleName string `json:"vehicleName,omitempty"` - LineDirection string `json:"lineDirection"` - Sens string `json:"sens,omitempty"` - Code string `json:"code,omitempty"` - Time string `json:"time"` - Schedule string `json:"schedule"` - Destination struct { - StopPointId string `json:"stopPointId"` - StopAreaId string `json:"stopAreaId"` - } `json:"destination,omitempty"` - Source string `json:"source,omitempty"` +type IDFMMonitoredStopVisit struct { + RecordedAtTime time.Time `json:"RecordedAtTime"` + ItemIdentifier string `json:"ItemIdentifier"` + MonitoringRef struct { + Value string `json:"value"` + } `json:"MonitoringRef"` + MonitoredVehicleJourney struct { + LineRef struct { + Value string `json:"value"` + } `json:"LineRef"` + OperatorRef struct { + Value string `json:"value"` + } `json:"OperatorRef"` + FramedVehicleJourneyRef struct { + DataFrameRef struct { + Value string `json:"value"` + } `json:"DataFrameRef"` + DatedVehicleJourneyRef string `json:"DatedVehicleJourneyRef"` + } `json:"FramedVehicleJourneyRef"` + DirectionName []struct { + Value string `json:"value"` + } `json:"DirectionName"` + DestinationRef struct { + Value string `json:"value"` + } `json:"DestinationRef"` + DestinationName []struct { + Value string `json:"value"` + } `json:"DestinationName"` + JourneyNote []struct { + Value string `json:"value"` + } `json:"JourneyNote"` + MonitoredCall struct { + StopPointName []struct { + Value string `json:"value"` + } `json:"StopPointName"` + VehicleAtStop bool `json:"VehicleAtStop"` + DestinationDisplay []struct { + Value string `json:"value"` + } `json:"DestinationDisplay"` + ExpectedArrivalTime time.Time `json:"ExpectedArrivalTime"` + ExpectedDepartureTime time.Time `json:"ExpectedDepartureTime"` + DepartureStatus string `json:"DepartureStatus"` + } `json:"MonitoredCall"` + } `json:"MonitoredVehicleJourney"` } type IDFMRealTime struct { - NextDepartures struct { - StatusCode int `json:"statusCode"` - ErrorMessage string `json:"errorMessage"` - Data []IDFMRealTimeData `json:"data"` - } `json:"nextDepartures"` - CrowdsourcingReports struct { - congestions []struct { - DirectionId string `json:"directionId"` - NearTimeReports struct { - Rating *string `json:"rating"` - } `json:"nearTimeReports"` - } `json:"congestions"` - } `json:"crowdsourcingReports"` -} - -type ByRealTime []IDFMRealTimeData - -func (s ByRealTime) Len() int { - return len(s) -} - -func (s ByRealTime) Less(i, j int) bool { - if s[i].Sens == s[j].Sens { - nj, err := strconv.Atoi(s[j].Time) - if err != nil { - return false - } - - ni, err := strconv.Atoi(s[i].Time) - if err != nil { - return true - } - - return ni < nj - } else { - return s[i].Sens < s[j].Sens - } -} - -func (s ByRealTime) Swap(i, j int) { - s[i], s[j] = s[j], s[i] + Siri struct { + ServiceDelivery struct { + ResponseTimestamp time.Time `json:"ResponseTimestamp"` + ProducerRef string `json:"ProducerRef"` + ResponseMessageIdentifier string `json:"ResponseMessageIdentifier"` + StopMonitoringDelivery []struct { + ResponseTimestamp time.Time `json:"ResponseTimestamp"` + Version string `json:"Version"` + Status string `json:"Status"` + MonitoredStopVisit []IDFMMonitoredStopVisit `json:"MonitoredStopVisit"` + } `json:"StopMonitoringDelivery"` + } `json:"ServiceDelivery"` + } `json:"siri"` } type PGSchedule struct { Destination string `json:"destination"` Message string `json:"message"` - Code string `json:"code,omitempty"` } func convertLineCode(code string) string { - return strings.TrimSuffix(strings.Replace(code, "STIF:Line::", "IDFM:", 1), ":") + return strings.TrimSuffix(code, ":") } -func getRealTime(code, station string) (*IDFMRealTime, error) { - rurl, err := url.JoinPath(IDFM_BASEURL, "lines", code, "stops", station, "realTime") +func getRealTime(code string, stations []string) ([]IDFMMonitoredStopVisit, error) { + rurl, err := url.JoinPath(IDFM_BASEURL, "stop-monitoring") if err != nil { return nil, err } @@ -98,101 +97,108 @@ func getRealTime(code, station string) (*IDFMRealTime, error) { return nil, err } - reqquery := url.Values{} - reqquery.Add("MonitoringRef", station) - requrl.RawQuery = reqquery.Encode() + var stops []IDFMMonitoredStopVisit + for _, station := range stations { + reqquery := url.Values{} + reqquery.Add("MonitoringRef", station) + reqquery.Add("LineRef", "STIF:Line::"+code+":") + requrl.RawQuery = reqquery.Encode() - req, err := http.NewRequest("GET", requrl.String(), nil) - if err != nil { - return nil, err + req, err := http.NewRequest("GET", requrl.String(), nil) + if err != nil { + return stops, err + } + + req.Header.Add("Accept", "application/json") + req.Header.Add("apikey", IDFM_TOKEN) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return stops, err + } + defer res.Body.Close() + + if res.StatusCode >= 400 { + v, _ := io.ReadAll(res.Body) + log.Println("Schedule not found: ", string(v)) + return nil, fmt.Errorf("Schedule not found") + } + + var schedules IDFMRealTime + dec := json.NewDecoder(res.Body) + if err = dec.Decode(&schedules); err != nil { + return stops, err + } + + for _, smd := range schedules.Siri.ServiceDelivery.StopMonitoringDelivery { + stops = append(stops, smd.MonitoredStopVisit...) + } } - req.Header.Add("Accept", "application/json") - req.Header.Add("apikey", IDFM_TOKEN) - - res, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode >= 400 { - v, _ := io.ReadAll(res.Body) - log.Println("Schedule not found: ", string(v)) - return nil, fmt.Errorf("Schedule not found") - } - - var schedules IDFMRealTime - - dec := json.NewDecoder(res.Body) - if err = dec.Decode(&schedules); err != nil { - return nil, err - } - - if schedules.NextDepartures.StatusCode >= 400 { - log.Println("Schedule not found: ", schedules) - return nil, fmt.Errorf("Schedule not found: %s", schedules.NextDepartures.ErrorMessage) - } - - return &schedules, nil + return stops, nil } func declareSchedulesRoutes(router *gin.RouterGroup) { router.GET("/schedules/:type/:code/:station/:way", func(c *gin.Context) { t := convertLineType(string(c.Param("type"))) - code := convertCode(t, string(c.Param("code"))) + code := searchLine(t, string(c.Param("code"))) station := string(c.Param("station")) way := string(c.Param("way")) - if !strings.HasPrefix(code, "line:IDFM:") { - if len(code) != 6 || !strings.HasPrefix(code, "C") { - code = searchLine(t, code) - } - - code = "line:IDFM:" + code - } - - if !strings.HasPrefix(station, "stop_area:IDFM:") { + var stations []string + if !strings.HasPrefix(station, "STIF:Stop") { if _, err := strconv.ParseInt(station, 10, 64); err != nil { - station, err = searchStation(code, station) + stations, err = searchStation(code, station) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } } else { - station = "stop_area:IDFM:" + station + stations = []string{"STIF:StopArea:IDFM:SP:" + station + ":"} + } + } else { + stations = []string{station} + } + + if way != "A+R" && len(stations) == 2 { + if way == "A" { + stations = []string{stations[0]} + } else { + stations = []string{stations[1]} } } - log.Println("search", code, station) - schedules, err := getRealTime(code, station) + schedules, err := getRealTime(code, stations) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } - sort.Sort(ByRealTime(schedules.NextDepartures.Data)) - pgs := []PGSchedule{} - for _, vehicule := range schedules.NextDepartures.Data { - if (way == "A" && vehicule.Sens == "1") || (way == "R" && vehicule.Sens == "-1") { - continue - } + for _, vehicule := range schedules { + msg := vehicule.MonitoredVehicleJourney.MonitoredCall.ExpectedDepartureTime.String() - msg := vehicule.Time + " mn" - - if vehicule.Code == "message" { - msg = vehicule.Schedule - } else if t == "rail" { - if n, err := strconv.Atoi(vehicule.Time); err == nil { - msg = time.Now().Add(time.Duration(n) * time.Minute).Format("15:04") + if t == "metro" || t == "bus" || t == "noctiliens" || t == "tramway" { + if vehicule.MonitoredVehicleJourney.MonitoredCall.VehicleAtStop { + if t == "metro" { + msg = "Train à quai" + } else { + msg = "A l'arret" + } + } else if time.Until(vehicule.MonitoredVehicleJourney.MonitoredCall.ExpectedDepartureTime) < 0 { + if t == "metro" { + msg = "Train retardé" + } else { + msg = "…" + } + } else { + msg = fmt.Sprintf("%d mn", int(math.Floor(time.Until(vehicule.MonitoredVehicleJourney.MonitoredCall.ExpectedDepartureTime).Minutes()))) } } pgs = append(pgs, PGSchedule{ - Destination: vehicule.LineDirection, + Destination: vehicule.MonitoredVehicleJourney.MonitoredCall.DestinationDisplay[0].Value, Message: msg, - Code: vehicule.VehicleName, }) } diff --git a/api/stations.go b/api/stations.go index a33166b..165c80a 100644 --- a/api/stations.go +++ b/api/stations.go @@ -2,10 +2,10 @@ package api import ( "encoding/json" - "fmt" "io/ioutil" + "log" "net/http" - "net/url" + "os" "strings" "unicode" @@ -16,14 +16,24 @@ import ( ) type IDFMStation struct { - Type string `json:"type"` - Id string `json:"id"` - X float64 `json:"x"` - Y float64 `json:"y"` - Name string `json:"name"` - ZipCode string `json:"zipCode"` - City string `json:"city"` - Elevator bool `json:"elevator"` + DatasetID string `json:"datasetid"` + RecordIDs string `json:"recordid"` + Fields struct { + Id string `json:"id"` + PointGeo []float64 `json:"pointgeo"` + StopId string `json:"stop_id"` + StopName string `json:"stop_name"` + OperatorName string `json:"operatorname"` + NomCommune string `json:"nom_commune"` + RouteLongName string `json:"route_long_name"` + StopLat string `json:"stop_lat"` + StopLon string `json:"stop_lon"` + CodeINSEE string `json:"code_insee"` + } `json:"fields"` + Geometry struct { + Type string `json:"type"` + Coordinates []float64 `json:"coordinates"` + } `json:"geometry"` } func (s *IDFMStation) ComputeSlug() string { @@ -32,99 +42,70 @@ func (s *IDFMStation) ComputeSlug() string { } t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) - r, _ := ioutil.ReadAll(transform.NewReader(strings.NewReader(s.Name), t)) + r, _ := ioutil.ReadAll(transform.NewReader(strings.NewReader(s.Fields.StopName), t)) return strings.ToLower(strings.Replace(strings.Replace(strings.Replace(string(r), " ", "+", -1), "'", "+", -1), "-", "+", -1)) } type PGStation struct { - Id string `json:"id"` + Id string `json:"slug"` Name string `json:"name"` - Slug string `json:"slug"` } var IDFMStations []IDFMStation -func searchStation(code, station string) (string, error) { - stations, err := getStations(code) +func init() { + fd, err := os.Open("arrets-lignes.json") if err != nil { - return station, err + log.Fatal("Unable to open `arrets-lignes.json`:", err.Error()) } + defer fd.Close() - code = strings.TrimPrefix(code, "line:IDFM:") + dec := json.NewDecoder(fd) + if err = dec.Decode(&IDFMStations); err != nil { + log.Fatal("Unable to decode `arrets-lignes.json`:", err.Error()) + } +} - for _, st := range stations { - if st.ComputeSlug() == station { - return st.Id, nil +func searchStation(code, station string) ([]string, error) { + code = "IDFM:" + strings.TrimPrefix(code, "line:IDFM:") + + var res []string + for _, st := range IDFMStations { + if st.Fields.Id == code && st.ComputeSlug() == station { + if strings.HasPrefix(st.Fields.StopId, "IDFM:monomodalStopPlace:") { + res = append(res, "STIF:StopArea:SP:"+strings.TrimPrefix(strings.TrimPrefix(st.Fields.StopId, "IDFM:"), "monomodalStopPlace:")+":") + } else { + res = append(res, "STIF:StopPoint:Q:"+strings.TrimPrefix(st.Fields.StopId, "IDFM:")+":") + } } } - return station, nil -} - -func getStations(code string) (stations []IDFMStation, err error) { - rurl, err := url.JoinPath(IDFM_BASEURL, "lines", code, "stops") - if err != nil { - return nil, err + if len(res) == 0 { + return []string{station}, nil + } else { + return res, nil } - - requrl, err := url.Parse(rurl) - if err != nil { - return nil, err - } - - reqquery := url.Values{} - reqquery.Add("stopPoints", "false") - reqquery.Add("routes", "false") - requrl.RawQuery = reqquery.Encode() - - req, err := http.NewRequest("GET", requrl.String(), nil) - if err != nil { - return nil, err - } - - req.Header.Add("Accept", "application/json") - - res, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode >= 400 { - return nil, fmt.Errorf("Line not found") - } - - dec := json.NewDecoder(res.Body) - if err = dec.Decode(&stations); err != nil { - return nil, err - } - - return } func declareStationsRoutes(router *gin.RouterGroup) { router.GET("/stations/:type/:code", func(c *gin.Context) { - t := convertLineType(string(c.Param("type"))) - code := convertCode(t, string(c.Param("code"))) + code := convertLineCode(string(c.Param("code"))) - stations, err := getStations(code) - if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) - return - } + var stations []PGStation + for _, station := range IDFMStations { + if station.Fields.Id == "IDFM:"+code { + pgstation := PGStation{ + Id: station.ComputeSlug(), + Name: station.Fields.StopName, + } - var pgs []PGStation - for _, station := range stations { - pgs = append(pgs, PGStation{ - Id: station.Id, - Name: station.Name, - Slug: station.ComputeSlug(), - }) + stations = append(stations, pgstation) + } } c.JSON(http.StatusOK, APIResult(c, map[string][]PGStation{ - "stations": pgs, + "stations": stations, })) }) } diff --git a/api/traffic.go b/api/traffic.go index bfec889..a63bd1c 100644 --- a/api/traffic.go +++ b/api/traffic.go @@ -44,7 +44,7 @@ func declareTrafficRoutes(router *gin.RouterGroup) { router.GET("/traffic/:type/:code", func(c *gin.Context) { code := searchLine(convertLineType(string(c.Param("type"))), string(c.Param("code"))) - rurl, err := url.JoinPath(IDFM2_BASEURL, "v2/navitia/lines", "line:IDFM:"+code, "line_reports") + rurl, err := url.JoinPath(IDFM_BASEURL, "v2/navitia/lines", "line:IDFM:"+code, "line_reports") if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return