Skip to content

Commit

Permalink
update aisle detection and deduplicate cabins
Browse files Browse the repository at this point in the history
  • Loading branch information
its-felix committed Nov 3, 2024
1 parent ef567be commit 27d7343
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 27 deletions.
8 changes: 4 additions & 4 deletions go/api/data/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ func (h *Handler) Airlines(ctx context.Context, prefix string) ([]common.Airline
}), nil
}

func (h *Handler) SeatMap(ctx context.Context, fn common.FlightNumber, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass lufthansa.CabinClass) (*lufthansa.SeatAvailability, error) {
func (h *Handler) SeatMap(ctx context.Context, fn common.FlightNumber, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass lufthansa.RequestCabinClass) (*lufthansa.SeatAvailability, error) {
sm, found, err := h.loadSeatMapFromS3(ctx, fn, departureAirport, arrivalAirport, departureDate, cabinClass)
if err != nil {
return nil, err
Expand Down Expand Up @@ -450,7 +450,7 @@ func (h *Handler) SeatMap(ctx context.Context, fn common.FlightNumber, departure
return sm, adapt.S3PutJson(ctx, h.s3c, h.bucket, s3Key, sm)
}

func (h *Handler) loadSeatMapFromLH(ctx context.Context, fn common.FlightNumber, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass lufthansa.CabinClass) (*lufthansa.SeatAvailability, error) {
func (h *Handler) loadSeatMapFromLH(ctx context.Context, fn common.FlightNumber, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass lufthansa.RequestCabinClass) (*lufthansa.SeatAvailability, error) {
sm, err := h.lhc.SeatMap(
ctx,
fn.String(),
Expand All @@ -472,7 +472,7 @@ func (h *Handler) loadSeatMapFromLH(ctx context.Context, fn common.FlightNumber,
return &sm, nil
}

func (h *Handler) loadSeatMapFromS3(ctx context.Context, fn common.FlightNumber, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass lufthansa.CabinClass) (*lufthansa.SeatAvailability, bool, error) {
func (h *Handler) loadSeatMapFromS3(ctx context.Context, fn common.FlightNumber, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass lufthansa.RequestCabinClass) (*lufthansa.SeatAvailability, bool, error) {
resp, err := h.s3c.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(h.bucket),
Key: aws.String(h.seatMapS3Key(fn, departureAirport, arrivalAirport, departureDate, cabinClass)),
Expand All @@ -496,7 +496,7 @@ func (h *Handler) loadSeatMapFromS3(ctx context.Context, fn common.FlightNumber,
return sm, true, nil
}

func (h *Handler) seatMapS3Key(fn common.FlightNumber, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass lufthansa.CabinClass) string {
func (h *Handler) seatMapS3Key(fn common.FlightNumber, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass lufthansa.RequestCabinClass) string {
return fmt.Sprintf("tmp/seatmap/%s/%s/%s/%s/%s.json", fn.String(), departureAirport, arrivalAirport, departureDate.String(), cabinClass)
}

Expand Down
9 changes: 7 additions & 2 deletions go/api/web/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,13 @@ func NewSeatMapEndpoint(dh *data.Handler) echo.HandlerFunc {
return echo.NewHTTPError(http.StatusNotFound)
}

cabinClasses := []lufthansa.CabinClass{lufthansa.CabinClassEco, lufthansa.CabinClassPremiumEco, lufthansa.CabinClassBusiness, lufthansa.CabinClassFirst}
rawSeatMaps := make(map[lufthansa.CabinClass]lufthansa.SeatAvailability)
cabinClasses := []lufthansa.RequestCabinClass{
lufthansa.RequestCabinClassEco,
lufthansa.RequestCabinClassPremiumEco,
lufthansa.RequestCabinClassBusiness,
lufthansa.RequestCabinClassFirst,
}
rawSeatMaps := make(map[lufthansa.RequestCabinClass]lufthansa.SeatAvailability)

for _, cabinClass := range cabinClasses {
sm, err := dh.SeatMap(
Expand Down
131 changes: 117 additions & 14 deletions go/api/web/seatmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,78 @@ func (rr RowRanges) Expand(other RowRanges) RowRanges {
return result
}

func normalizeSeatMaps(rawSeatMaps map[lufthansa.CabinClass]lufthansa.SeatAvailability) SeatMap {
func deduplicateSeatMaps(rawSeatMaps map[lufthansa.RequestCabinClass]lufthansa.SeatAvailability) {
type Key struct {
cc lufthansa.RequestCabinClass
rows lufthansa.SeatDisplayRows
}

type NonMatch struct {
cc lufthansa.RequestCabinClass
generalized bool
}

match := make(common.Set[Key])
nonMatch := make(map[Key][]NonMatch)

for cc, seatMap := range rawSeatMaps {
for _, sd := range seatMap.SeatDisplay {
generalizedCC, ok := generalizeCabinType(sd.CabinType)
key := Key{generalizedCC, sd.Rows}

if ok && cc == generalizedCC {
match[key] = struct{}{}
} else {
nonMatch[key] = append(nonMatch[key], NonMatch{cc, ok})
}
}
}

for key, nonMatchedRequestCCs := range nonMatch {
if !match.Contains(key) {
if len(nonMatchedRequestCCs) > 1 {
// only remove if there is more than 1 non-matches, keeping the first successfully generalized one, if any
idx := slices.IndexFunc(nonMatchedRequestCCs, func(nm NonMatch) bool {
return nm.generalized
})

if idx == -1 {
nonMatchedRequestCCs = nonMatchedRequestCCs[1:]
} else {
// replace element at index with the last in slice
nonMatchedRequestCCs[idx] = nonMatchedRequestCCs[len(nonMatchedRequestCCs)-1]
// remove the last element
nonMatchedRequestCCs = nonMatchedRequestCCs[:len(nonMatchedRequestCCs)-1]
}
} else {
// if there is only one, do not remove any
nonMatchedRequestCCs = nil
}
}

for _, nonMatchedRequestCC := range nonMatchedRequestCCs {
if sm, ok := rawSeatMaps[nonMatchedRequestCC.cc]; ok {
sm.SeatDisplay = slices.DeleteFunc(sm.SeatDisplay, func(sd lufthansa.SeatDisplay) bool {
return sd.Rows == key.rows
})

sm.SeatDetails = slices.DeleteFunc(sm.SeatDetails, func(sd lufthansa.SeatDetail) bool {
return sd.Location.Row.Number >= key.rows.First && sd.Location.Row.Number <= key.rows.Last
})

if len(sm.SeatDetails) > 0 || len(sm.SeatDisplay) > 0 {
rawSeatMaps[nonMatchedRequestCC.cc] = sm
} else {
delete(rawSeatMaps, nonMatchedRequestCC.cc)
}
}
}
}
}

func normalizeSeatMaps(rawSeatMaps map[lufthansa.RequestCabinClass]lufthansa.SeatAvailability) SeatMap {
deduplicateSeatMaps(rawSeatMaps)

sm := SeatMap{
CabinClasses: make(common.Set[string]),
Decks: make([]*SeatMapDeck, 0),
Expand Down Expand Up @@ -241,20 +312,20 @@ func normalizeSeatMapCabin(cabinClass string, sd lufthansa.SeatDisplay, details

slices.SortFunc(cabin.ComponentColumns, func(a, b ColumnIdentifier) int {
idx := func(v string) int {
switch v {
case string(lufthansa.ComponentColumnCharacteristicLeftSide):
switch lufthansa.ComponentColumnCharacteristic(v) {
case lufthansa.ComponentColumnCharacteristicLeftSide:
return 0

case string(lufthansa.ComponentColumnCharacteristicLeftCenter):
case lufthansa.ComponentColumnCharacteristicLeftCenter:
return 1

case string(lufthansa.ComponentColumnCharacteristicCenter):
case lufthansa.ComponentColumnCharacteristicCenter:
return 2

case string(lufthansa.ComponentColumnCharacteristicRightCenter):
case lufthansa.ComponentColumnCharacteristicRightCenter:
return 3

case string(lufthansa.ComponentColumnCharacteristicRightSide):
case lufthansa.ComponentColumnCharacteristicRightSide:
return 4
}

Expand Down Expand Up @@ -357,7 +428,7 @@ func isLeftOfAisle(i int, details []lufthansa.SeatDetail) bool {
} else if sd.HasCharacteristic(lufthansa.SeatCharacteristicExitRow) {
// ignore for exit row, those tend to be special
return false
} else if !sd.HasCharacteristic(lufthansa.SeatCharacteristicAisle) {
} else if !sd.AisleAccess() {
return false
}

Expand All @@ -377,7 +448,7 @@ func isNextToAisle(i int, details []lufthansa.SeatDetail) bool {
return true
}

return details[i].HasCharacteristic(lufthansa.SeatCharacteristicAisle)
return details[i].AisleAccess()
}

func appendSeat(columns []string, rows []SeatMapRow, currRow SeatMapRow, sd lufthansa.SeatDetail) (SeatMapRow, []SeatMapRow) {
Expand Down Expand Up @@ -452,20 +523,52 @@ func appendComponent(seatColumns []string, componentColumns []ColumnIdentifier,
return currRow, rows
}

func normalizeCabinClass(cc lufthansa.CabinClass) string {
func normalizeCabinClass(cc lufthansa.RequestCabinClass) string {
switch cc {
case lufthansa.CabinClassEco:
case lufthansa.RequestCabinClassEco:
return "ECO"

case lufthansa.CabinClassPremiumEco:
case lufthansa.RequestCabinClassPremiumEco:
return "PRECO"

case lufthansa.CabinClassBusiness:
case lufthansa.RequestCabinClassBusiness:
return "BIZ"

case lufthansa.CabinClassFirst:
case lufthansa.RequestCabinClassFirst:
return "FIRST"
}

return string(cc)
}

func generalizeCabinType(cabinType lufthansa.Code) (lufthansa.RequestCabinClass, bool) {
switch lufthansa.RequestCabinClass(cabinType) {
case lufthansa.RequestCabinClassFirst:
return lufthansa.RequestCabinClassFirst, true

case lufthansa.RequestCabinClassBusiness:
return lufthansa.RequestCabinClassBusiness, true

case lufthansa.RequestCabinClassPremiumEco:
return lufthansa.RequestCabinClassPremiumEco, true

case lufthansa.RequestCabinClassEco:
return lufthansa.RequestCabinClassEco, true
}

switch lufthansa.CabinClass(cabinType) {
case lufthansa.CabinClassFirstClass, lufthansa.CabinClassFirstClassPremium, lufthansa.CabinClassFirstClassDiscounted:
return lufthansa.RequestCabinClassFirst, true

case lufthansa.CabinClassBusinessClass, lufthansa.CabinClassBusinessClassPremium, lufthansa.CabinClassBusinessClassDiscounted:
return lufthansa.RequestCabinClassBusiness, true

case lufthansa.CabinClassCoachEconomyPremium:
return lufthansa.RequestCabinClassPremiumEco, true

case lufthansa.CabinClassCoachEconomy, lufthansa.CabinClassCoachEconomyDiscounted1, lufthansa.CabinClassCoachEconomyDiscounted2, lufthansa.CabinClassCoachEconomyDiscounted3, lufthansa.CabinClassCoachEconomyDiscounted4, lufthansa.CabinClassCoachEconomyDiscounted5:
return lufthansa.RequestCabinClassEco, true
}

return lufthansa.RequestCabinClass(cabinType), false
}
6 changes: 3 additions & 3 deletions go/common/lufthansa/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func (c *Client) FlightSchedulesRaw(ctx context.Context, airlines []common.Airli
return err
}

func (c *Client) SeatMap(ctx context.Context, fn, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass CabinClass) (SeatAvailability, error) {
func (c *Client) SeatMap(ctx context.Context, fn, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass RequestCabinClass) (SeatAvailability, error) {
path := c.buildSeatMapPath(fn, departureAirport, arrivalAirport, departureDate, cabinClass)
data, err := doRequest[seatAvailabilityResource](ctx, c, http.MethodGet, path, nil, nil, readJsonFunc[seatAvailabilityResource]())
if err != nil {
Expand All @@ -234,12 +234,12 @@ func (c *Client) SeatMap(ctx context.Context, fn, departureAirport, arrivalAirpo
return data.Inner.SeatAvailability, nil
}

func (c *Client) SeatMapRaw(ctx context.Context, fn, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass CabinClass) (json.RawMessage, error) {
func (c *Client) SeatMapRaw(ctx context.Context, fn, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass RequestCabinClass) (json.RawMessage, error) {
path := c.buildSeatMapPath(fn, departureAirport, arrivalAirport, departureDate, cabinClass)
return doRequest[json.RawMessage](ctx, c, http.MethodGet, path, nil, nil, readJsonFunc[json.RawMessage]())
}

func (c *Client) buildSeatMapPath(fn, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass CabinClass) string {
func (c *Client) buildSeatMapPath(fn, departureAirport, arrivalAirport string, departureDate xtime.LocalDate, cabinClass RequestCabinClass) string {
return fmt.Sprintf(
"/v1/offers/seatmaps/%s/%s/%s/%s/%s",
url.PathEscape(fn),
Expand Down
35 changes: 31 additions & 4 deletions go/common/lufthansa/seatmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,35 @@ import (
)

type CabinClass string
type RequestCabinClass CabinClass

const (
CabinClassFirst = CabinClass("F")
CabinClassBusiness = CabinClass("C")
CabinClassPremiumEco = CabinClass("E")
CabinClassEco = CabinClass("M")
CabinClassFirstClassDiscounted = CabinClass("A")
CabinClassCoachEconomyDiscounted1 = CabinClass("B")
CabinClassBusinessClass = CabinClass("C")
CabinClassBusinessClassDiscounted = CabinClass("D")
CabinClassShuttleService1 = CabinClass("E")
CabinClassFirstClass = CabinClass("F")
CabinClassConditionalReservation = CabinClass("G")
CabinClassCoachEconomyDiscounted2 = CabinClass("H")
CabinClassBusinessClassPremium = CabinClass("J")
CabinClassThrift = CabinClass("K")
CabinClassThriftDiscounted1 = CabinClass("L")
CabinClassCoachEconomyDiscounted3 = CabinClass("M")
CabinClassFirstClassPremium = CabinClass("P")
CabinClassCoachEconomyDiscounted4 = CabinClass("Q")
CabinClassSupersonic = CabinClass("R")
CabinClassStandardClass = CabinClass("S")
CabinClassCoachEconomyDiscounted5 = CabinClass("T")
CabinClassShuttleService2 = CabinClass("U")
CabinClassThriftDiscounted2 = CabinClass("V")
CabinClassCoachEconomyPremium = CabinClass("W")
CabinClassCoachEconomy = CabinClass("Y")

RequestCabinClassFirst = RequestCabinClass("F")
RequestCabinClassBusiness = RequestCabinClass("C")
RequestCabinClassPremiumEco = RequestCabinClass("E")
RequestCabinClassEco = RequestCabinClass("M")
)

type SeatCharacteristic Code
Expand Down Expand Up @@ -180,6 +203,10 @@ func (sd SeatDetail) HasCharacteristic(c SeatCharacteristic) bool {
return slices.Contains(sd.Location.Row.Characteristics.Characteristic, c)
}

func (sd SeatDetail) AisleAccess() bool {
return sd.HasCharacteristic(SeatCharacteristicAisle) || sd.HasCharacteristic(SeatCharacteristicWindowAndAisleTogether)
}

func (sd SeatDetail) Empty() bool {
return sd.HasCharacteristic(SeatCharacteristicNoSeatAtLocation) || sd.Location.Column == "" || len(sd.Location.Row.Characteristics.Characteristic) < 1
}
Expand Down

0 comments on commit 27d7343

Please sign in to comment.