model/situation.go

Summary

Maintainability
D
1 day
Test Coverage
package model

import (
    "encoding/json"
    "errors"
    "fmt"
    "sync"
    "time"

    e "bitbucket.org/enroute-mobi/ara/core/apierrs"
    "bitbucket.org/enroute-mobi/ara/gtfs"
    "bitbucket.org/enroute-mobi/ara/logger"
    "bitbucket.org/enroute-mobi/ara/uuid"
    "github.com/sym01/htmlsanitizer"
)

type SituationId ModelId

const (
    SituationReportTypeGeneral  ReportType    = "general"
    SituationReportTypeIncident ReportType    = "incident"
    SituationTypeLine           SituationType = "Line"
    SituationTypeStopArea       SituationType = "StopArea"
    SituationTypeAllLines       SituationType = "AllLines"
)

type ReportType string
type SituationType string

type TranslatedString struct {
    DefaultValue string            `json:",omitempty"`
    Translations map[string]string `json:",omitempty"`
}

type Situation struct {
    model Model
    CodeConsumer
    id     SituationId
    Origin string

    RecordedAt time.Time
    Version    int `json:",omitempty"`

    VersionedAt        time.Time
    ValidityPeriods    []*TimeRange
    PublicationWindows []*TimeRange

    Progress       SituationProgress   `json:",omitempty"`
    Severity       SituationSeverity   `json:",omitempty"`
    Keywords       []string            `json:",omitempty"`
    ReportType     ReportType          `json:",omitempty"`
    AlertCause     SituationAlertCause `json:",omitempty"`
    Reality        SituationReality    `json:",omitempty"`
    ProducerRef    string              `json:",omitempty"`
    Format         string              `json:",omitempty"`
    InternalTags   []string            `json:",omitempty"`
    ParticipantRef string              `json:",omitempty"`
    Summary        *TranslatedString   `json:",omitempty"`
    Description    *TranslatedString   `json:",omitempty"`

    Affects                 Affects                   `json:",omitempty"`
    Consequences            []*Consequence            `json:",omitempty"`
    PublishToWebActions     []*PublishToWebAction     `json:",omitempty"`
    PublishToMobileActions  []*PublishToMobileAction  `json:",omitempty"`
    PublishToDisplayActions []*PublishToDisplayAction `json:",omitempty"`
}

type ActionData struct {
    Name       string            `json:",omitempty"`
    ActionType string            `json:",omitempty"`
    Value      string            `json:",omitempty"`
    Prompt     *TranslatedString `json:",omitempty"`
}

type ActionCommon struct {
    Name               string                `json:",omitempty"`
    ActionType         string                `json:",omitempty"`
    Value              string                `json:",omitempty"`
    Prompt             *TranslatedString     `json:",omitempty"`
    ScopeType          SituationScopeType    `json:",omitempty"`
    Affects            Affects               `json:",omitempty"`
    ActionStatus       SituationActionStatus `json:",omitempty"`
    Description        *TranslatedString     `json:",omitempty"`
    PublicationWindows []*TimeRange          `json:",omitempty"`
}

type PublishToWebAction struct {
    ActionCommon

    Incidents      *bool    `json:",omitempty"`
    HomePage       *bool    `json:",omitempty"`
    Ticker         *bool    `json:",omitempty"`
    SocialNetworks []string `json:",omitempty"`
}

type PublishToMobileAction struct {
    ActionCommon

    Incidents *bool `json:",omitempty"`
    HomePage  *bool `json:",omitempty"`
}

type PublishToDisplayAction struct {
    ActionCommon

    OnPlace *bool `json:",omitempty"`
    OnBoard *bool `json:",omitempty"`
}

type Consequence struct {
    Periods   []*TimeRange       `json:",omitempty"`
    Condition SituationCondition `json:",omitempty"`
    Severity  SituationSeverity  `json:",omitempty"`
    Affects   Affects            `json:",omitempty"`
    Blocking  *Blocking          `json:",omitempty"`
}

type Blocking struct {
    JourneyPlanner bool
    RealTime       bool
}

// SubTypes of Affect
type Affect interface {
    GetType() SituationType
    GetId() ModelId
}

type Affects []Affect

type AffectedStopArea struct {
    StopAreaId StopAreaId `json:",omitempty"`
    LineIds    []LineId   `json:",omitempty"`
}

func (a AffectedStopArea) GetId() ModelId {
    return ModelId(a.StopAreaId)
}

func (a AffectedStopArea) GetType() SituationType {
    return SituationTypeStopArea
}

func NewAffectedStopArea() *AffectedStopArea {
    return &AffectedStopArea{}
}

type AffectedAllLines struct{}

func (a AffectedAllLines) GetId() ModelId {
    return ModelId("")
}

func (a AffectedAllLines) GetType() SituationType {
    return SituationTypeAllLines
}

func NewAffectedAllLines() *AffectedAllLines {
    return &AffectedAllLines{}
}

type AffectedLine struct {
    LineId               LineId                 `json:",omitempty"`
    AffectedDestinations []*AffectedDestination `json:",omitempty"`
    AffectedSections     []*AffectedSection     `json:",omitempty"`
    AffectedRoutes       []*AffectedRoute       `json:",omitempty"`
}

type AffectedDestination struct {
    StopAreaId StopAreaId
}

type AffectedSection struct {
    FirstStop StopAreaId
    LastStop  StopAreaId
}

type AffectedRoute struct {
    RouteRef    string       `json:",omitempty"`
    StopAreaIds []StopAreaId `json:",omitempty"`
}

func (a AffectedLine) GetId() ModelId {
    return ModelId(a.LineId)
}

func (a AffectedLine) GetType() SituationType {
    return SituationTypeLine
}

func NewAffectedLine() *AffectedLine {
    return &AffectedLine{}
}

type TimeRange struct {
    StartTime time.Time
    EndTime   time.Time
}

func NewSituation(model Model) *Situation {
    situation := &Situation{
        model: model,
    }

    situation.codes = make(Codes)
    return situation
}

func (situation *Situation) Id() SituationId {
    return situation.id
}

func (situation *Situation) Save() (ok bool) {
    ok = situation.model.Situations().Save(situation)
    return
}

func (affects *Affects) UnmarshalJSON(data []byte) error {
    var raw []json.RawMessage
    err := json.Unmarshal(data, &raw)
    if err != nil {
        return err
    }

    *affects = make(Affects, len(raw))
    for i, v := range raw {
        var affectedSubtype = struct {
            Type SituationType
        }{}
        err = json.Unmarshal(v, &affectedSubtype)
        if err != nil {
            return err
        }
        switch affectedSubtype.Type {
        case SituationTypeStopArea:
            a := NewAffectedStopArea()
            json.Unmarshal(v, a)
            (*affects)[i] = a
        case SituationTypeLine:
            l := NewAffectedLine()
            json.Unmarshal(v, l)
            (*affects)[i] = l
        case SituationTypeAllLines:
            all := NewAffectedAllLines()
            json.Unmarshal(v, all)
            (*affects)[i] = all
        }
    }
    return nil
}

func (apiSituation *APISituation) MarshalJSON() ([]byte, error) {
    type Alias APISituation
    aux := struct {
        *Alias
        Codes  Codes    `json:",omitempty"`
        Errors e.Errors `json:"Errors,omitempty"`
    }{
        Alias: (*Alias)(apiSituation),
    }

    if !apiSituation.Codes().Empty() {
        aux.Codes = apiSituation.Codes()
    }

    if !apiSituation.Errors.Empty() {
        aux.Errors = apiSituation.Errors
    }
    return json.Marshal(&aux)
}

func (situation *APISituation) UnmarshalJSON(data []byte) error {
    type Alias APISituation

    aux := &struct {
        Codes map[string]string
        *Alias
    }{
        Alias: (*Alias)(situation),
    }

    err := json.Unmarshal(data, aux)
    if err != nil {
        return err
    }

    if aux.Codes != nil {
        situation.CodeConsumer.codes = NewCodesFromMap(aux.Codes)
    }

    return nil
}

func (affect AffectedStopArea) MarshalJSON() ([]byte, error) {
    type Alias AffectedStopArea
    aux := struct {
        Type SituationType
        Alias
    }{
        Type:  SituationTypeStopArea,
        Alias: (Alias)(affect),
    }

    return json.Marshal(&aux)
}

func (affect AffectedLine) MarshalJSON() ([]byte, error) {
    type Alias AffectedLine
    aux := struct {
        Type SituationType
        Alias
    }{
        Type:  SituationTypeLine,
        Alias: (Alias)(affect),
    }

    return json.Marshal(&aux)
}

func (affect AffectedAllLines) MarshalJSON() ([]byte, error) {
        type Alias AffectedAllLines
    aux := struct {
        Type SituationType
        Alias
    }{
        Type:  SituationTypeAllLines,
        Alias: (Alias)(affect),
    }

    return json.Marshal(&aux)
}

func (t *TimeRange) UnmarshalJSON(data []byte) error {
    aux := &struct {
        StartTime *time.Time
        EndTime   *time.Time
    }{}

    err := json.Unmarshal(data, aux)
    if err != nil {
        return err
    }

    if aux.StartTime == nil {
        t.StartTime = time.Time{}
    } else {
        t.StartTime = *aux.StartTime
    }

    if aux.EndTime == nil {
        t.EndTime = time.Time{}
    } else {
        t.EndTime = *aux.EndTime
    }

    return nil
}

func (t *TimeRange) MarshalJSON() ([]byte, error) {
    type Alias TimeRange
    aux := struct {
        StartTime *time.Time `json:",omitempty"`
        EndTime   *time.Time `json:",omitempty"`
        *Alias
    }{
        Alias: (*Alias)(t),
    }

    if !t.StartTime.IsZero() {
        aux.StartTime = &t.StartTime
    }

    if !t.EndTime.IsZero() {
        aux.EndTime = &t.EndTime
    }

    return json.Marshal(&aux)
}

func (situation *Situation) modelId() ModelId {
    return ModelId(situation.id)
}

func (situation *Situation) MarshalJSON() ([]byte, error) {
    type Alias Situation
    aux := struct {
        Codes       Codes      `json:",omitempty"`
        RecordedAt  *time.Time `json:",omitempty"`
        VersionedAt *time.Time `json:",omitempty"`
        *Alias
        Id      SituationId
        Affects []Affect `json:",omitempty"`
    }{
        Id:    situation.id,
        Alias: (*Alias)(situation),
    }

    if !situation.Codes().Empty() {
        aux.Codes = situation.Codes()
    }

    if !situation.RecordedAt.IsZero() {
        aux.RecordedAt = &situation.RecordedAt
    }

    if !situation.VersionedAt.IsZero() {
        aux.VersionedAt = &situation.VersionedAt
    }

    if len(situation.Affects) != 0 {
        aux.Affects = situation.Affects
    }

    return json.Marshal(&aux)
}

func (situation *Situation) GMValidUntil() time.Time {
    if len(situation.ValidityPeriods) == 0 {
        return time.Time{}
    }
    return situation.ValidityPeriods[0].EndTime
}

func (situation *Situation) GetGMChannel() (string, bool) {
    switch {
    case situation.containsKeyword("Perturbation"):
        return "Perturbation", true
    case situation.containsKeyword("Information"):
        return "Information", true
    case situation.containsKeyword("Commercial"):
        return "Commercial", true
    default:
        return "", false
    }
}

func (situation *Situation) containsKeyword(str string) bool {
    if len(situation.Keywords) == 0 {
        return false
    }
    for _, v := range situation.Keywords {
        if v == str {
            return true
        }
    }
    return false
}

type APISituation struct {
    Id     SituationId `json:",omitempty"`
    Origin string      `json:",omitempty"`
    CodeConsumer

    CodeSpace             string    `json:",omitempty"`
    SituationNumber       string    `json:",omitempty"`
    ExistingSituationCode bool      `json:"-"`
    RecordedAt            time.Time `json:",omitempty"`
    Version               int       `json:",omitempty"`

    VersionedAt        time.Time    `json:",omitempty"`
    ValidityPeriods    []*TimeRange `json:",omitempty"`
    PublicationWindows []*TimeRange `json:",omitempty"`

    Progress       SituationProgress   `json:",omitempty"`
    Severity       SituationSeverity   `json:",omitempty"`
    Reality        SituationReality    `json:",omitempty"`
    Keywords       []string            `json:",omitempty"`
    ReportType     ReportType          `json:",omitempty"`
    AlertCause     SituationAlertCause `json:",omitempty"`
    ProducerRef    string              `json:",omitempty"`
    Format         string              `json:",omitempty"`
    InternalTags   []string            `json:",omitempty"`
    ParticipantRef string              `json:",omitempty"`
    Summary        *TranslatedString   `json:",omitempty"`
    Description    *TranslatedString   `json:",omitempty"`

    Affects                 Affects                   `json:",omitempty"`
    Consequences            []*Consequence            `json:",omitempty"`
    PublishToWebActions     []*PublishToWebAction     `json:",omitempty"`
    PublishToMobileActions  []*PublishToMobileAction  `json:",omitempty"`
    PublishToDisplayActions []*PublishToDisplayAction `json:",omitempty"`

    Errors e.Errors `json:"Errors,omitempty"`

    IgnoreValidation bool `json:",omitempty"`
}

func (apiSituation *APISituation) Validate() bool {
    if apiSituation.CodeSpace == "" {
        apiSituation.Errors.Add("CodeSpace", e.ERROR_BLANK)
    }

    if apiSituation.SituationNumber == "" {
        apiSituation.Errors.Add("SituationNumber", e.ERROR_BLANK)
    }

    if apiSituation.ExistingSituationCode {
        apiSituation.Errors.Add("SituationNumber", e.ERROR_UNIQUE)
    }

    if apiSituation.Version == 0 && apiSituation.Id == "" {
        apiSituation.Errors.Add("Version", e.ERROR_BLANK)
    }

    if apiSituation.Summary != nil {
        if apiSituation.Summary.DefaultValue == "" {
            apiSituation.Errors.Add("Summary", e.ERROR_BLANK)
        }
    }

    sanitizer := htmlsanitizer.NewHTMLSanitizer()
    if apiSituation.Summary != nil {
        sanitizedSummary, err := sanitizer.Sanitize([]byte(apiSituation.Summary.DefaultValue))
        if err != nil {
            apiSituation.Errors.Add("Summary", fmt.Sprintf("%s: %v", e.ERROR_FORMAT, err))
        } else {
            apiSituation.Summary.DefaultValue = string(sanitizedSummary)
        }
    }

    if apiSituation.Description != nil {
        sanitizedDescription, err := sanitizer.Sanitize([]byte(apiSituation.Description.DefaultValue))
        if err != nil {
            apiSituation.Errors.Add("Description", fmt.Sprintf("%s: %v", e.ERROR_FORMAT, err))
        } else {
            apiSituation.Description.DefaultValue = string(sanitizedDescription)
        }
    }

    if len(apiSituation.ValidityPeriods) == 0 {
        apiSituation.Errors.Add("ValidityPeriods", e.ERROR_BLANK)
    }

    for _, period := range apiSituation.ValidityPeriods {
        if period.StartTime.IsZero() {
            apiSituation.Errors.Add("ValidityPeriods", e.ERROR_BLANK)
            break
        }
    }

    if len(apiSituation.Affects) == 0 {
        apiSituation.Errors.Add("Affects", e.ERROR_BLANK)
    }

    return len(apiSituation.Errors) == 0
}

func (situation *Situation) Definition() *APISituation {
    apiSituation := &APISituation{
        Id:                     situation.Id(),
        Affects:                []Affect{},
        AlertCause:             situation.AlertCause,
        Consequences:           []*Consequence{},
        Description:            situation.Description,
        Errors:                 e.NewErrors(),
        Format:                 situation.Format,
        InternalTags:           situation.InternalTags,
        Keywords:               situation.Keywords,
        Origin:                 situation.Origin,
        ParticipantRef:         situation.ParticipantRef,
        PublishToWebActions:    []*PublishToWebAction{},
        PublishToMobileActions: []*PublishToMobileAction{},
        PublishToDisplayActions: []*PublishToDisplayAction{},
        ProducerRef:            situation.ProducerRef,
        Progress:               situation.Progress,
        PublicationWindows:     situation.PublicationWindows,
        Reality:                situation.Reality,
        RecordedAt:             situation.RecordedAt,
        ReportType:             situation.ReportType,
        Severity:               situation.Severity,
        Summary:                situation.Summary,
        ValidityPeriods:        situation.ValidityPeriods,
        Version:                situation.Version,
        VersionedAt:            situation.VersionedAt,
        IgnoreValidation:       false,
    }

    apiSituation.codes = make(Codes)
    return apiSituation
}

func (situation *Situation) SetDefinition(apiSituation *APISituation) {
    situation.Affects = apiSituation.Affects
    situation.AlertCause = apiSituation.AlertCause
    situation.Consequences = apiSituation.Consequences
    situation.Description = apiSituation.Description
    situation.Format = apiSituation.Format
    situation.InternalTags = apiSituation.InternalTags
    situation.Keywords = apiSituation.Keywords
    situation.Origin = apiSituation.Origin
    situation.ParticipantRef = apiSituation.ParticipantRef
    situation.PublishToWebActions = apiSituation.PublishToWebActions
    situation.PublishToMobileActions = apiSituation.PublishToMobileActions
    situation.PublishToDisplayActions = apiSituation.PublishToDisplayActions
    situation.ProducerRef = apiSituation.ProducerRef
    situation.Progress = apiSituation.Progress
    situation.PublicationWindows = apiSituation.PublicationWindows
    situation.Reality = apiSituation.Reality
    situation.RecordedAt = apiSituation.RecordedAt
    situation.ReportType = apiSituation.ReportType
    situation.Severity = apiSituation.Severity
    situation.Summary = apiSituation.Summary
    situation.ValidityPeriods = apiSituation.ValidityPeriods
    situation.Version = apiSituation.Version
    situation.VersionedAt = apiSituation.VersionedAt

    if apiSituation.codes.Empty() {
        if apiSituation.CodeSpace != "" && apiSituation.SituationNumber != "" {
            situation.codes = make(Codes)
            code := NewCode(apiSituation.CodeSpace, apiSituation.SituationNumber)
            situation.SetCode(code)
        }
    } else {
        // keep cucumber scenarios compatibility with API
        situation.codes = apiSituation.codes
    }
}

type MemorySituations struct {
    uuid.UUIDConsumer

    model *MemoryModel

    mutex            *sync.RWMutex
    GMbroadcastEvent func(event SituationBroadcastEvent)
    SXbroadcastEvent func(event SituationBroadcastEvent)
    byIdentifier     map[SituationId]*Situation
}

type Situations interface {
    uuid.UUIDInterface

    New() Situation
    Find(id SituationId) (Situation, bool)
    FindByCode(code Code) (Situation, bool)
    FindAll() []Situation
    Save(situation *Situation) bool
    Delete(situation *Situation) bool
}

func NewMemorySituations() *MemorySituations {
    return &MemorySituations{
        mutex:        &sync.RWMutex{},
        byIdentifier: make(map[SituationId]*Situation),
    }
}

func (manager *MemorySituations) New() Situation {
    situation := NewSituation(manager.model)
    return *situation
}

func (manager *MemorySituations) Find(id SituationId) (Situation, bool) {
    manager.mutex.RLock()
    situation, ok := manager.byIdentifier[id]
    manager.mutex.RUnlock()

    if ok {
        return *situation, true
    }
    return Situation{}, false
}

func (manager *MemorySituations) FindAll() (situations []Situation) {
    manager.mutex.RLock()
    defer manager.mutex.RUnlock()

    if len(manager.byIdentifier) == 0 {
        return []Situation{}
    }
    for _, situation := range manager.byIdentifier {
        situations = append(situations, *situation)
    }
    return
}

func (manager *MemorySituations) FindByCode(code Code) (Situation, bool) {
    manager.mutex.RLock()
    defer manager.mutex.RUnlock()

    for _, situation := range manager.byIdentifier {
        situationCode, _ := situation.Code(code.CodeSpace())
        if situationCode.Value() == code.Value() {
            return *situation, true
        }
    }
    return Situation{}, false
}

func (manager *MemorySituations) Save(situation *Situation) bool {
    manager.mutex.Lock()
    defer manager.mutex.Unlock()

    if situation.Id() == "" {
        situation.id = SituationId(manager.NewUUID())
    }
    situation.model = manager.model
    manager.byIdentifier[situation.Id()] = situation

    event := SituationBroadcastEvent{
        SituationId: situation.id,
    }

    if manager.GMbroadcastEvent != nil {
        manager.GMbroadcastEvent(event)
    }

    if manager.SXbroadcastEvent != nil {
        manager.SXbroadcastEvent(event)
    }
    return true
}

func (manager *MemorySituations) Delete(situation *Situation) bool {
    manager.mutex.Lock()
    defer manager.mutex.Unlock()

    delete(manager.byIdentifier, situation.Id())
    return true
}

func NewTranslatedStringFromMap(translations map[string]string) *TranslatedString {
    ts := &TranslatedString{
        Translations: make(map[string]string),
    }

    for lang, text := range translations {
        if lang == "" {
            ts.DefaultValue = text
            continue
        }
        ts.Translations[lang] = text
    }

    return ts
}

func NewTranslatedStringFromProto(value []*gtfs.TranslatedString_Translation) *TranslatedString {
    ts := &TranslatedString{
        Translations: make(map[string]string),
    }

    for _, translation := range value {
        if translation.GetLanguage() == "" {
            ts.DefaultValue = translation.GetText()
            continue
        }

        ts.Translations[translation.GetLanguage()] = translation.GetText()
    }

    return ts
}

func (ts *TranslatedString) ToProto(dest *gtfs.TranslatedString) {
    translations := []*gtfs.TranslatedString_Translation{}
    if ts.DefaultValue != "" {
        var emptyLanguage string
        gtfsTranslation := &gtfs.TranslatedString_Translation{
            Language: &emptyLanguage,
            Text:     &ts.DefaultValue,
        }

        translations = append(translations, gtfsTranslation)
    }

    for lang, text := range ts.Translations {
        gtfsTranslation := &gtfs.TranslatedString_Translation{
            Language: &lang,
            Text:     &text,
        }

        translations = append(translations, gtfsTranslation)
    }

    dest.Translation = translations
}

func (t *TimeRange) FromProto(value interface{}) error {
    var timeRange TimeRange
    switch v := value.(type) {
    case *gtfs.TimeRange:
        if v.GetStart() == 0 {
            return errors.New("gtfs.Timerange missing Start")
        }
        timeRange.StartTime = time.Unix(int64(v.GetStart()), 0)

        if v.GetEnd() != 0 {
            timeRange.EndTime = time.Unix(int64(v.GetEnd()), 0)
        }

    default:
        return fmt.Errorf("unsupported value %T", value)
    }

    *t = timeRange
    return nil
}

func (t *TimeRange) ToProto(dest interface{}) error {
    if t == nil {
        return errors.New("nil TimeRange")
    }

    switch v := dest.(type) {
    case *gtfs.TimeRange:
        if start := t.StartTime; !start.IsZero() {
            startTime := uint64(start.Unix())
            v.Start = &startTime
        }
        if end := t.EndTime; !end.IsZero() {
            endTime := uint64(end.Unix())
            v.End = &endTime
        }
    default:
        return fmt.Errorf("unsupported value %T", dest)
    }
    return nil
}

type AffectRefs struct {
    MonitoringRefs map[string]struct{}
    LineRefs       map[string]struct{}
}

func AffectFromProto(value interface{}, remoteCodeSpace string, m Model) (Affect, *AffectRefs, error) {
    collectedRefs := &AffectRefs{
        MonitoringRefs: make(map[string]struct{}),
        LineRefs:       make(map[string]struct{}),
    }

    switch v := value.(type) {
    case *gtfs.EntitySelector:
        lineId := v.GetRouteId()
        stopId := v.GetStopId()

        if stopId != "" {
            stopCode := NewCode(remoteCodeSpace, stopId)
            stopArea, ok := m.StopAreas().FindByCode(stopCode)
            if !ok {
                return nil, nil, fmt.Errorf("unknow stopId: %v", stopId)
            }
            affect := NewAffectedStopArea()
            affect.StopAreaId = stopArea.Id()
            collectedRefs.MonitoringRefs[stopId] = struct{}{}
            if lineId != "" {
                lineCode := NewCode(remoteCodeSpace, lineId)
                line, ok := m.Lines().FindByCode(lineCode)
                if ok {
                    affect.LineIds = append(affect.LineIds, line.Id())
                    collectedRefs.LineRefs[lineId] = struct{}{}
                }
            }
            return affect, collectedRefs, nil
        }

        if lineId != "" {
            lineCode := NewCode(remoteCodeSpace, lineId)
            line, ok := m.Lines().FindByCode(lineCode)
            if !ok {
                return nil, nil, fmt.Errorf("unknow lineId: %v", lineId)
            }
            affect := NewAffectedLine()
            affect.LineId = line.Id()
            collectedRefs.LineRefs[lineId] = struct{}{}
            return affect, collectedRefs, nil
        }
    default:
        return nil, nil, fmt.Errorf("invalide type: %T", value)
    }
    return nil, nil, errors.New("cannot find line/stopArea model from gtfs ")
}

func AffectToProto(a Affect, remoteCodeSpace string, m Model) ([]*gtfs.EntitySelector, *AffectRefs, error) {
    collectedRefs := &AffectRefs{
        MonitoringRefs: make(map[string]struct{}),
        LineRefs:       make(map[string]struct{}),
    }
    var entities []*gtfs.EntitySelector

    switch v := a.(type) {
    case *AffectedLine:
        line, ok := m.Lines().Find(v.LineId)
        if !ok {
            return nil, nil, fmt.Errorf("unknown lineId: %v", v.LineId)
        }

        lineCode, ok := line.ReferentOrSelfCode(remoteCodeSpace)
        if !ok {
            return nil, nil, fmt.Errorf("lineId %v does not have right codeSpace %v", v.LineId, remoteCodeSpace)
        }

        var routeId *string
        value := lineCode.Value()
        routeId = &value

        collectedRefs.LineRefs[lineCode.Value()] = struct{}{}
        entities = append(entities, &gtfs.EntitySelector{RouteId: routeId})

        return entities, collectedRefs, nil
    case *AffectedStopArea:
        sa, ok := m.StopAreas().Find(v.StopAreaId)
        if !ok {
            return nil, nil, fmt.Errorf("unknown stopAreaId: %v", v.StopAreaId)
        }

        saCode, ok := sa.ReferentOrSelfCode(remoteCodeSpace)
        if !ok {
            return nil, nil, fmt.Errorf("stopId %v does not have right codeSpace %v", v.StopAreaId, remoteCodeSpace)
        }

        for i := range v.LineIds {
            line, ok := m.Lines().Find(v.LineIds[i])
            if !ok {
                logger.Log.Debugf("unknown line id: %v", v.LineIds[i])
                continue
            }
            lineCode, ok := line.ReferentOrSelfCode(remoteCodeSpace)
            if !ok {
                logger.Log.Debugf("line id: %v does not have right codeSpace %v", v.LineIds[i], remoteCodeSpace)
                continue
            }
            var stopId *string
            saValue := saCode.Value()
            stopId = &saValue

            var lineId *string
            lineValue := lineCode.Value()
            lineId = &lineValue

            e := &gtfs.EntitySelector{StopId: stopId, RouteId: lineId}
            collectedRefs.LineRefs[lineCode.Value()] = struct{}{}
            collectedRefs.MonitoringRefs[*stopId] = struct{}{}
            entities = append(entities, e)
        }
        var stopId *string
        value := saCode.Value()
        stopId = &value

        collectedRefs.MonitoringRefs[saCode.Value()] = struct{}{}
        entities = append(entities, &gtfs.EntitySelector{StopId: stopId})
        return entities, collectedRefs, nil
    }
    return nil, nil, fmt.Errorf("unsupported value: %T", a)
}