diff --git a/api/lines.go b/api/lines.go index f04692b..3d745b7 100644 --- a/api/lines.go +++ b/api/lines.go @@ -82,6 +82,16 @@ func convertLineType(old string) string { } } +func searchLine(t, code string) string { + for _, line := range IDFMLines { + if line.Fields.ShortName == code && (line.Fields.TransportMode == t || line.Fields.NetworkName == t) { + return line.Fields.IdLine + } + } + + return code +} + func declareLinesRoutes(router *gin.RouterGroup) { router.GET("/lines", func(c *gin.Context) { var modes []string @@ -121,10 +131,10 @@ func declareLinesRoutes(router *gin.RouterGroup) { } pgline := PGLine{ - Code: fmt.Sprintf("STIF:Line::%s:", line.Fields.IdLine), + Code: line.Fields.ShortName, Name: name, Directions: "", - Id: line.Fields.IdLine, + Id: fmt.Sprintf("STIF:Line::%s:", line.Fields.IdLine), } lines = append(lines, pgline) diff --git a/api/schedules.go b/api/schedules.go index e6c3eb1..0bfdbf7 100644 --- a/api/schedules.go +++ b/api/schedules.go @@ -2,88 +2,50 @@ package api import ( "encoding/json" - "flag" - "fmt" - "math" "net/http" "net/url" + "strconv" "strings" "time" "github.com/gin-gonic/gin" ) -const IDFM_BASEURL = "https://prim.iledefrance-mobilites.fr/marketplace" - -var IDFM_TOKEN = "" - -func init() { - flag.StringVar(&IDFM_TOKEN, "token-IDFM", IDFM_TOKEN, "Token to access IDFM API") -} +const IDFM_BASEURL = "https://api-iv.iledefrance-mobilites.fr/" type IDFMSchedule struct { - 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 []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"` - } `json:"MonitoredStopVisit"` - } `json:"StopMonitoringDelivery"` - } `json:"ServiceDelivery"` - } `json:"siri"` + NextDepartures struct { + StatusCode int `json:"statusCode"` + Data []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"` + } `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 PGSchedule struct { Destination string `json:"destination"` Message string `json:"message"` + Code string `json:"code,omitempty"` } func convertLineCode(code string) string { @@ -92,11 +54,32 @@ func convertLineCode(code string) string { func declareSchedulesRoutes(router *gin.RouterGroup) { router.GET("/schedules/:type/:code/:station/:way", func(c *gin.Context) { - t := string(c.Param("type")) - line := string(c.Param("code")) - station := convertLineType(string(c.Param("station"))) + t := convertLineType(string(c.Param("type"))) + code := convertLineType(string(c.Param("code"))) + station := string(c.Param("station")) + way := string(c.Param("way")) - rurl, err := url.JoinPath(IDFM_BASEURL, "stop-monitoring") + 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:") { + if _, err := strconv.ParseInt(station, 10, 64); err != nil { + station, err = searchStation(code, station) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + } else { + station = "stop_area:IDFM:" + station + } + } + + rurl, err := url.JoinPath(IDFM_BASEURL, "lines", code, "stops", station, "realTime") if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return @@ -128,6 +111,11 @@ func declareSchedulesRoutes(router *gin.RouterGroup) { } defer res.Body.Close() + if res.StatusCode >= 400 { + c.AbortWithStatusJSON(res.StatusCode, APIResult(c, nil)) + return + } + var schedules IDFMSchedule dec := json.NewDecoder(res.Body) @@ -137,34 +125,25 @@ func declareSchedulesRoutes(router *gin.RouterGroup) { } pgs := []PGSchedule{} - for _, vehicule := range schedules.Siri.ServiceDelivery.StopMonitoringDelivery[0].MonitoredStopVisit { - if len(line) > 0 && vehicule.MonitoredVehicleJourney.LineRef.Value != line { + for _, vehicule := range schedules.NextDepartures.Data { + if (way == "A" && vehicule.Sens == "-1") || (way == "R" && vehicule.Sens == "1") { continue } - msg := vehicule.MonitoredVehicleJourney.MonitoredCall.ExpectedDepartureTime.String() + msg := vehicule.Time + " mn" - if t == "metros" || t == "buses" || t == "noctiliens" || t == "tramways" { - if vehicule.MonitoredVehicleJourney.MonitoredCall.VehicleAtStop { - if t == "metros" { - msg = "Train à quai" - } else { - msg = "A l'arret" - } - } else if time.Until(vehicule.MonitoredVehicleJourney.MonitoredCall.ExpectedDepartureTime) < 0 { - if t == "metros" { - msg = "Train retardé" - } else { - msg = "…" - } - } else { - msg = fmt.Sprintf("%d mn", int(math.Floor(time.Until(vehicule.MonitoredVehicleJourney.MonitoredCall.ExpectedDepartureTime).Minutes()))) + 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") } } pgs = append(pgs, PGSchedule{ - Destination: vehicule.MonitoredVehicleJourney.MonitoredCall.DestinationDisplay[0].Value, + Destination: vehicule.LineDirection, Message: msg, + Code: vehicule.VehicleName, }) } diff --git a/api/stations.go b/api/stations.go index c55e73a..75f6db9 100644 --- a/api/stations.go +++ b/api/stations.go @@ -3,73 +3,136 @@ package api import ( "encoding/json" "fmt" - "log" + "io/ioutil" "net/http" - "os" + "net/url" "strings" + "unicode" "github.com/gin-gonic/gin" + + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" ) type IDFMStation struct { - 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"` + 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"` +} + +func (s *IDFMStation) ComputeSlug() string { + isMn := func(r rune) bool { + return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks + } + t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) + + r, _ := ioutil.ReadAll(transform.NewReader(strings.NewReader(s.Name), t)) + + return strings.ToLower(strings.Replace(strings.Replace(strings.Replace(string(r), " ", "+", -1), "'", "+", -1), "-", "+", -1)) } type PGStation struct { - Id string `json:"slug"` + Id string `json:"id"` Name string `json:"name"` + Slug string `json:"slug"` } var IDFMStations []IDFMStation -func init() { - fd, err := os.Open("arrets-lignes.json") +func searchStation(code, station string) (string, error) { + stations, err := getStations(code) if err != nil { - log.Fatal("Unable to open `arrets-lignes.json`:", err.Error()) + return station, err } - defer fd.Close() - dec := json.NewDecoder(fd) - if err = dec.Decode(&IDFMStations); err != nil { - log.Fatal("Unable to decode `arrets-lignes.json`:", err.Error()) + code = strings.TrimPrefix(code, "line:IDFM:") + + for _, st := range stations { + if st.ComputeSlug() == station { + return st.Id, nil + } } + + 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 + } + + 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) { - code := convertLineCode(string(c.Param("code"))) + t := convertLineType(string(c.Param("type"))) + code := string(c.Param("code")) - var stations []PGStation - for _, station := range IDFMStations { - if station.Fields.Id == code { - pgstation := PGStation{ - Id: fmt.Sprintf("STIF:StopPoint:Q:%s:", strings.TrimPrefix(station.Fields.StopId, "IDFM:")), - Name: station.Fields.StopName, - } - - stations = append(stations, pgstation) + if !strings.HasPrefix(code, "line:IDFM:") { + if len(code) != 6 || !strings.HasPrefix(code, "C") { + code = searchLine(t, code) } + + code = "line:IDFM:" + code + } + + stations, err := getStations(code) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + var pgs []PGStation + for _, station := range stations { + pgs = append(pgs, PGStation{ + Id: station.Id, + Name: station.Name, + Slug: station.ComputeSlug(), + }) } c.JSON(http.StatusOK, APIResult(c, map[string][]PGStation{ - "stations": stations, + "stations": pgs, })) }) } diff --git a/api/traffic.go b/api/traffic.go index 43d34a5..30fd7c7 100644 --- a/api/traffic.go +++ b/api/traffic.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "flag" "net/http" "net/url" "time" @@ -9,6 +10,14 @@ import ( "github.com/gin-gonic/gin" ) +const IDFM2_BASEURL = "https://prim.iledefrance-mobilites.fr/marketplace" + +var IDFM_TOKEN = "" + +func init() { + flag.StringVar(&IDFM_TOKEN, "token-IDFM", IDFM_TOKEN, "Token to access IDFM API") +} + type IDFMTraffic struct { Siri struct { ServiceDelivery struct { @@ -78,7 +87,7 @@ func declareTrafficRoutes(router *gin.RouterGroup) { router.GET("/traffic/:type/:code", func(c *gin.Context) { code := convertLineType(string(c.Param("code"))) - rurl, err := url.JoinPath(IDFM_BASEURL, "general-message") + rurl, err := url.JoinPath(IDFM2_BASEURL, "general-message") if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return @@ -91,7 +100,7 @@ func declareTrafficRoutes(router *gin.RouterGroup) { } reqquery := url.Values{} - reqquery.Add("LineRef", code) + reqquery.Add("LineRef", "STIF:Line::"+code+":") requrl.RawQuery = reqquery.Encode() req, err := http.NewRequest("GET", requrl.String(), nil)