mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Merge branch 'main' into upstream/nominatim-cache-mode-windows
This commit is contained in:
28 files changed
+1053
-11
No files matched your search
@@ -0,0 +1,505 @@
|
||||
/**
|
||||
* Rio de Janeiro route ids to the line a rider would say.
|
||||
*
|
||||
* SMTR's realtime feeds send internal keys (`O0731AAN0A` is SN731,
|
||||
* `20000501110` is BRT line 50). They look decodable but are not:
|
||||
* `O0014AAA0A` is LECD135, not 14. The table is copied from `routes.txt` in
|
||||
* SMTR's static GTFS (https://dados.mobilidade.rio/gtfs/schedule, CC BY 4.0),
|
||||
* valid to 2026-12-31. An id missing here renders as its raw key.
|
||||
*/
|
||||
export const RIO_ROUTE_NAMES = Object.freeze({
|
||||
20000101130: '10',
|
||||
20000111110: '11',
|
||||
20000121130: '12',
|
||||
20000131130: '13',
|
||||
20000141130: '14',
|
||||
20000151130: '15',
|
||||
20000161130: '16',
|
||||
20000171110: '17',
|
||||
20000181130: '18',
|
||||
20000191130: '19',
|
||||
20000201130: '20',
|
||||
20000221110: '22',
|
||||
20000251130: '25',
|
||||
20000281130: '28',
|
||||
20000311140: '31',
|
||||
20000351110: '35',
|
||||
20000401130: '40',
|
||||
20000411130: '41',
|
||||
20000421110: '42',
|
||||
20000431130: '43',
|
||||
20000461130: '46',
|
||||
20000501110: '50',
|
||||
20000511110: '51',
|
||||
20000521110: '52',
|
||||
20000531130: '53',
|
||||
20000601110: '60',
|
||||
20000611130: '61',
|
||||
20000671130: '67',
|
||||
20000681130: '68',
|
||||
20000701110: '70',
|
||||
20000731130: '73',
|
||||
20000801110: '80',
|
||||
20000901110: '90',
|
||||
'20000EXEC1110': 'ESP01',
|
||||
E2110AAA0A: '2110',
|
||||
E2111AAA0A: '2111',
|
||||
E2112AAA0A: '2112',
|
||||
E2114AAA0A: '2114',
|
||||
E2303AAA0A: '2303',
|
||||
E2305AAA0A: '2305',
|
||||
E2307AAA0A: '2307',
|
||||
E2308AAA0A: '2308',
|
||||
E2309AAA0A: '2309',
|
||||
E2310AAA0A: '2310',
|
||||
E2334AAA0A: '2334',
|
||||
E2335AAA0A: '2335',
|
||||
E2336AAA0A: '2336',
|
||||
E2337AAA0A: '2337',
|
||||
E2338AAA0A: '2338',
|
||||
E2339AAA0A: '2339',
|
||||
E2342AAA0A: '2342',
|
||||
E2343AAA0A: '2343',
|
||||
E2344AAA0A: '2344',
|
||||
E2345AAA0A: '2345',
|
||||
E2381AAA0A: '2381',
|
||||
E2383AAA0A: '2383',
|
||||
E2613AAA0A: '2613',
|
||||
E2614AAA0A: '2614',
|
||||
E2802AAA0A: '2802',
|
||||
E2803AAA0A: '2803',
|
||||
E2804AAA0A: '2804',
|
||||
O0006AAA0A: '006',
|
||||
O0007AAA0A: '007',
|
||||
O0007AAN0A: 'SN007',
|
||||
O0010AAA0A: '010',
|
||||
O0014AAA0A: 'LECD135',
|
||||
O0100AAA0A: '100',
|
||||
O0104AAA0A: '104',
|
||||
O0104AAN0A: 'SN104',
|
||||
O0107AAA0A: '107',
|
||||
O0109AAA0A: 'LECD131',
|
||||
O0111AAA0A: 'LECD134',
|
||||
O0117AAA0A: '117',
|
||||
O0133AAA0A: '133',
|
||||
O0133AAN0A: 'SN133',
|
||||
O0157AAA0A: 'LECD127',
|
||||
O0160AAA0A: '160',
|
||||
O0161AAA0A: '161',
|
||||
O0162AAA0A: '162',
|
||||
O0164AAA0A: 'LECD136',
|
||||
O0165AAA0A: '165',
|
||||
O0167AAA0A: '167',
|
||||
O0169AAA0A: '169',
|
||||
O0201AAA0A: '201',
|
||||
O0202AAA0A: '202',
|
||||
O0209AAA0A: '209',
|
||||
O0209AAN0A: 'SN209',
|
||||
O0210AAA0A: '210',
|
||||
O0217AAA0A: '217',
|
||||
O0220AAA0A: '220',
|
||||
O0229AAA0A: '229',
|
||||
O0232AAA0A: '232',
|
||||
O0232AAN0A: 'SN232',
|
||||
O0238AAA0A: '238',
|
||||
O0238AAN0A: 'SN238',
|
||||
O0239AAA0A: '239',
|
||||
O0247AAA0A: '247',
|
||||
O0249AAA0A: '249',
|
||||
O0254AAA0A: '254',
|
||||
O0265AAA0A: '265',
|
||||
O0265AAN0A: 'SN265',
|
||||
O0265AAP0A: 'SP265',
|
||||
O0275AAA0A: '275',
|
||||
O0277AAA0A: '277',
|
||||
O0292AAA0A: '292',
|
||||
O0292AAN0A: 'SN292',
|
||||
O0296AAA0A: '296',
|
||||
O0298AAA0A: '298',
|
||||
O0298AAN0A: 'SN298',
|
||||
O0300AAA0A: '300',
|
||||
O0300AAR0A: 'SR300',
|
||||
O0301AAA0A: '301',
|
||||
O0302AAA0A: '302',
|
||||
O0302AAN0A: 'SN302',
|
||||
O0306AAA0A: '306',
|
||||
O0311AAA0A: '311',
|
||||
O0312AAA0A: '312',
|
||||
O0312AAN0A: 'SN312',
|
||||
O0313AAA0A: '313',
|
||||
O0315AAA0A: '315',
|
||||
O0319AAA0A: '319',
|
||||
O0319AAN0A: 'SN319',
|
||||
O0319AAV0A: 'SV319',
|
||||
O0321AAA0A: '321',
|
||||
O0323AAA0A: '323',
|
||||
O0324AAA0A: '324',
|
||||
O0324AAN0A: 'SN324',
|
||||
O0324AAP0A: 'SP324',
|
||||
O0325AAA0A: '325',
|
||||
O0326AAA0A: '326',
|
||||
O0326AAPBA: 'SPB326',
|
||||
O0327AAA0A: '327',
|
||||
O0328AAA0A: '328',
|
||||
O0328AAN0A: 'SN328',
|
||||
O0328AAP0A: 'SP328',
|
||||
O0328AAR0A: 'SR328',
|
||||
O0329AAA0A: '329',
|
||||
O0329AAP0A: 'SP329',
|
||||
O0335AAA0A: '335',
|
||||
O0338AAA0A: '338',
|
||||
O0341AAA0A: '341',
|
||||
O0342AAA0A: '342',
|
||||
O0342AAR0A: 'SR342',
|
||||
O0343AAA0A: '343',
|
||||
O0343AAP0A: 'SP343',
|
||||
O0348AAA0A: '348',
|
||||
O0349AAA0A: '349',
|
||||
O0350AAA0A: '350',
|
||||
O0352AAA0A: '352',
|
||||
O0353AAA0A: '353',
|
||||
O0353AAN0A: 'SN353',
|
||||
O0355AAA0A: '355',
|
||||
O0355AAN0A: 'SN355',
|
||||
O0355AAR0A: 'SR355',
|
||||
O0361AAA0A: '361',
|
||||
O0362AAA0A: '362',
|
||||
O0362AAR0A: 'SR362',
|
||||
O0363AAA0A: '363',
|
||||
O0363AAN0A: 'SN363',
|
||||
O0363AAV0A: 'SV363',
|
||||
O0368AAA0A: '368',
|
||||
O0368AAN0A: 'SN368',
|
||||
O0369AAA0A: '369',
|
||||
O0371AAA0A: '371',
|
||||
O0378AAA0A: '378',
|
||||
O0378AAR0A: 'SR378',
|
||||
O0380AAA0A: '380',
|
||||
O0383AAA0A: '383',
|
||||
O0383AAN0A: 'SN383',
|
||||
O0384AAA0A: '384',
|
||||
O0384AAR0A: 'SR384',
|
||||
O0385AAA0A: '385',
|
||||
O0386AAA0A: '386',
|
||||
O0386AAR0A: 'SR386',
|
||||
O0388AAA0A: '388',
|
||||
O0388AAN0A: 'SN388',
|
||||
O0388AAR0A: 'SR388',
|
||||
O0389AAP0A: 'SP389',
|
||||
O0390AAA0A: '390',
|
||||
O0391AAV0A: 'SV391',
|
||||
O0393AAA0A: '393',
|
||||
O0393AAN0A: 'SN393',
|
||||
O0393AAR0A: 'SR393',
|
||||
O0397AAA0A: '397',
|
||||
O0397AAN0A: 'SN397',
|
||||
O0397AAR0A: 'SR397',
|
||||
O0399AAA0A: '399',
|
||||
O0399AAN0A: 'SN399',
|
||||
O0399AAR0A: 'SR399',
|
||||
O0404AAP0A: 'SP404',
|
||||
O0409AAA0A: '409',
|
||||
O0410AAA0A: '410',
|
||||
O0410AAN0A: 'LECD139',
|
||||
O0415AAA0A: '415',
|
||||
O0415AAN0A: 'SN415',
|
||||
O0416AAA0A: '416',
|
||||
O0422AAA0A: '422',
|
||||
O0422AAN0A: 'SN422',
|
||||
O0426AAA0A: '426',
|
||||
O0433AAA0A: 'LECD150',
|
||||
O0435AAA0A: 'LECD151',
|
||||
O0448AAA0A: '448',
|
||||
O0455AAA0A: '455',
|
||||
O0456AAA0A: 'LECD132',
|
||||
O0457AAA0A: '457',
|
||||
O0457AAN0A: 'SN457',
|
||||
O0461AAA0A: '461',
|
||||
O0472AAA0A: '472',
|
||||
O0473AAA0A: '473',
|
||||
O0474AAA0A: '474',
|
||||
O0474AAN0A: 'SN474',
|
||||
O0474AAV0A: 'SV474',
|
||||
O0475AAA0A: 'LECD133',
|
||||
O0476AAA0A: '476',
|
||||
O0483AAA0A: '483',
|
||||
O0483AAN0A: 'SN483',
|
||||
O0483AAPAA: 'SPA483',
|
||||
O0483AAPBA: 'SPB483',
|
||||
O0483AAV0A: 'SV483',
|
||||
O0484AAA0A: '484',
|
||||
O0485AAA0A: '485',
|
||||
O0485AAP0A: 'SP485',
|
||||
O0486AAA0A: '486',
|
||||
O0492AAA0A: '492',
|
||||
O0497AAA0A: '497',
|
||||
O0497AAN0A: 'SN497',
|
||||
O0498AAA0A: '498',
|
||||
O0507AAA0A: '507',
|
||||
O0513AAA0A: '513',
|
||||
O0518AAA0A: '518',
|
||||
O0519AAA0A: '519',
|
||||
O0536AAA0A: 'LECD138',
|
||||
O0539AAA0A: '539',
|
||||
O0550AAA0A: '550',
|
||||
O0550AAN0A: 'SN550',
|
||||
O0550AAPAA: 'SPA550',
|
||||
O0550AAPBA: 'SPB550',
|
||||
O0552AAA0A: '552',
|
||||
O0552AAN0A: 'LECD149',
|
||||
O0553AAA0A: '553',
|
||||
O0554AAA0A: '554',
|
||||
O0554AAN0A: 'SN554',
|
||||
O0555AAA0A: '555',
|
||||
O0557AAA0A: '557',
|
||||
O0565AAA0A: '565',
|
||||
O0565AAN0A: 'SN565',
|
||||
O0583AAA0A: '583',
|
||||
O0584AAA0A: '584',
|
||||
O0585AAA0A: '585',
|
||||
O0600AAA0A: '600',
|
||||
O0600AAN0A: 'SN600',
|
||||
O0601AAA0A: '601',
|
||||
O0603AAA0A: '603',
|
||||
O0606AAA0A: '606',
|
||||
O0606AAV0A: 'SV606',
|
||||
O0607AAA0A: '607',
|
||||
O0608AAA0A: '608',
|
||||
O0610AAA0A: '610',
|
||||
O0611AAA0A: '611',
|
||||
O0613AAA0A: '613',
|
||||
O0613AAPAA: 'SPA613',
|
||||
O0614AAA0A: '614',
|
||||
O0616AAA0A: '616',
|
||||
O0621AAA0A: '621',
|
||||
O0622AAA0A: '622',
|
||||
O0623AAA0A: '623',
|
||||
O0624AAA0A: '624',
|
||||
O0624AAN0A: 'SN624',
|
||||
O0624AAV0A: 'SV624',
|
||||
O0625AAA0A: '625',
|
||||
O0626AAA0A: '626',
|
||||
O0627AAA0A: '627',
|
||||
O0628AAA0A: '628',
|
||||
O0629AAA0A: '629',
|
||||
O0629AAN0A: 'SN629',
|
||||
O0630AAA0A: '630',
|
||||
O0634AAA0A: '634',
|
||||
O0635AAA0A: '635',
|
||||
O0635AAP0A: 'SP635',
|
||||
O0636AAA0A: '636',
|
||||
O0636AAN0A: 'SN636',
|
||||
O0638AAA0A: '638',
|
||||
O0639AAA0A: '639',
|
||||
O0639AAN0A: 'SN639',
|
||||
O0645AAA0A: '645',
|
||||
O0649AAA0A: 'LECD126',
|
||||
O0650AAA0A: '650',
|
||||
O0651AAA0A: '651',
|
||||
O0652AAA0A: '652',
|
||||
O0653AAA0A: '653',
|
||||
O0663AAA0A: '663',
|
||||
O0665AAA0A: '665',
|
||||
O0665AAVAA: 'LECD147',
|
||||
O0665AAVBA: 'SVB665',
|
||||
O0669AAA0A: '669',
|
||||
O0669AAV0A: 'SV669',
|
||||
O0678AAA0A: '678',
|
||||
O0679AAA0A: '679',
|
||||
O0685AAA0A: 'LECD140',
|
||||
O0685AAVAA: 'LECD148',
|
||||
O0685AAVBA: 'LECD141',
|
||||
O0687AAA0A: '687',
|
||||
O0688AAA0A: '688',
|
||||
O0691AAA0A: '691',
|
||||
O0692AAA0A: '692',
|
||||
O0692AAV0A: 'SV692',
|
||||
O0693AAA0A: '693',
|
||||
O0696AAA0A: '696',
|
||||
O0696AAN0A: 'SN696',
|
||||
O0711AAA0A: '711',
|
||||
O0712AAA0A: '712',
|
||||
O0721AAA0A: '721',
|
||||
O0731AAA0A: '731',
|
||||
O0731AAN0A: 'SN731',
|
||||
O0737AAA0A: '737',
|
||||
O0741AAA0A: '741',
|
||||
O0743AAA0A: '743',
|
||||
O0745AAA0A: '745',
|
||||
O0746AAA0A: '746',
|
||||
O0753AAA0A: '753',
|
||||
O0754AAA0A: '754',
|
||||
O0756AAA0A: '756',
|
||||
O0757AAA0A: '757',
|
||||
O0759AAA0A: '759',
|
||||
O0759AAP0A: 'SP759',
|
||||
O0764AAA0A: '764',
|
||||
O0765AAA0A: '765',
|
||||
O0766AAA0A: '766',
|
||||
O0766AAP0A: 'SP766',
|
||||
O0770AAA0A: '770',
|
||||
O0771AAA0A: '771',
|
||||
O0774AAA0A: '774',
|
||||
O0774AAV0A: 'LECD142',
|
||||
O0775AAA0A: '775',
|
||||
O0777AAV0A: 'SV777',
|
||||
O0778AAA0A: '778',
|
||||
O0779AAA0A: '779',
|
||||
O0779AAN0A: 'SN779',
|
||||
O0780AAA0A: '780',
|
||||
O0783AAA0A: '783',
|
||||
O0785AAA0A: '785',
|
||||
O0786AAA0A: '786',
|
||||
O0788AAA0A: '788',
|
||||
O0790AAA0A: '790',
|
||||
O0790AAN0A: 'SN790',
|
||||
O0790AAV0A: 'SV790',
|
||||
O0791AAA0A: '791',
|
||||
O0793AAA0A: '793',
|
||||
O0794AAA0A: '794',
|
||||
O0794AAN0A: 'SN794',
|
||||
O0795AAP0A: 'SP795',
|
||||
O0796AAA0A: '796',
|
||||
O0798AAA0A: '798',
|
||||
O0799AAA0A: '799',
|
||||
O0801AAA0A: '801',
|
||||
O0801AAP0A: 'SP801',
|
||||
O0802AAA0A: '802',
|
||||
O0803AAA0A: '803',
|
||||
O0803AAN0A: 'SN803',
|
||||
O0803AAP0A: 'SP803',
|
||||
O0804AAA0A: '804',
|
||||
O0805AAA0A: '805',
|
||||
O0805AAN0A: 'SN805',
|
||||
O0805AAP0A: 'SP805',
|
||||
O0807AAA0A: '807',
|
||||
O0808AAA0A: '808',
|
||||
O0809AAA0A: '809',
|
||||
O0810AAA0A: '810',
|
||||
O0810AAN0A: 'SN810',
|
||||
O0810AAP0A: 'SP810',
|
||||
O0812AAA0A: '812',
|
||||
O0817AAA0A: '817',
|
||||
O0819AAA0A: '819',
|
||||
O0820AAA0A: '820',
|
||||
O0821AAA0A: '821',
|
||||
O0821AAN0A: 'SN821',
|
||||
O0822AAA0A: '822',
|
||||
O0825AAA0A: '825',
|
||||
O0825AAN0A: 'SN825',
|
||||
O0826AAA0A: '826',
|
||||
O0829AAA0A: '829',
|
||||
O0830AAA0A: '830',
|
||||
O0831AAA0A: '831',
|
||||
O0831AAV0A: 'SV831',
|
||||
O0833AAA0A: '833',
|
||||
O0834AAA0A: '834',
|
||||
O0835AAA0A: '835',
|
||||
O0836AAA0A: '836',
|
||||
O0837AAA0A: '837',
|
||||
O0838AAA0A: '838',
|
||||
O0840AAA0A: '840',
|
||||
O0840AAN0A: 'SN840',
|
||||
O0841AAA0A: '841',
|
||||
O0842AAA0A: '842',
|
||||
O0843AAA0A: '843',
|
||||
O0844AAA0A: '844',
|
||||
O0845AAA0A: '845',
|
||||
O0846AAA0A: '846',
|
||||
O0846AAN0A: 'SN846',
|
||||
O0847AAA0A: '847',
|
||||
O0848AAA0A: '848',
|
||||
O0848AAN0A: 'SN848',
|
||||
O0849AAA0A: '849',
|
||||
O0850AAA0A: '850',
|
||||
O0851AAA0A: '851',
|
||||
O0852AAA0A: '852',
|
||||
O0853AAA0A: '853',
|
||||
O0853AAN0A: 'SN853',
|
||||
O0853AAV0A: 'SV853',
|
||||
O0854AAAAA: '854',
|
||||
O0855AAA0A: '855',
|
||||
O0856AAA0A: '856',
|
||||
O0857AAA0A: '857',
|
||||
O0857AAN0A: 'SN857',
|
||||
O0859AAA0A: '859',
|
||||
O0861AAA0A: '861',
|
||||
O0861AAN0A: 'SN861',
|
||||
O0862AAA0A: '862',
|
||||
O0863AAA0A: '863',
|
||||
O0863AAV0A: 'SV863',
|
||||
O0864AAA0A: '864',
|
||||
O0864AAP0A: 'SP864',
|
||||
O0865AAA0A: '865',
|
||||
O0866AAA0A: '866',
|
||||
O0866AAV0A: 'SV866',
|
||||
O0867AAA0A: '867',
|
||||
O0867AAN0A: 'SN867',
|
||||
O0868AAA0A: '868',
|
||||
O0869AAA0A: '869',
|
||||
O0870AAA0A: '870',
|
||||
O0871AAA0A: '871',
|
||||
O0872AAA0A: '872',
|
||||
O0878AAA0A: '878',
|
||||
O0878AAN0A: 'SN878',
|
||||
O0878AAV0A: 'SV878',
|
||||
O0880AAA0A: '880',
|
||||
O0880AAN0A: 'SN880',
|
||||
O0881AAA0A: '881',
|
||||
O0883AAA0A: '883',
|
||||
O0884AAA0A: '884',
|
||||
O0884AAN0A: 'SN884',
|
||||
O0885AAA0A: '885',
|
||||
O0885AAN0A: 'SN885',
|
||||
O0892AAA0A: '892',
|
||||
O0893AAA0A: '893',
|
||||
O0893AAN0A: 'SN893',
|
||||
O0894AAA0A: '894',
|
||||
O0895AAA0A: '895',
|
||||
O0895AAN0A: 'SN895',
|
||||
O0897AAA0A: '897',
|
||||
O0897AAN0A: 'SN897',
|
||||
O0898AAA0A: '898',
|
||||
O0898AAN0A: 'SN898',
|
||||
O0899AAA0A: '899',
|
||||
O0899AAV0A: 'SV899',
|
||||
O0900AAA0A: '900',
|
||||
O0901AAA0A: '901',
|
||||
O0901AAVBA: 'SVB901',
|
||||
O0905AAA0A: '905',
|
||||
O0906AAA0A: '906',
|
||||
O0908AAA0A: '908',
|
||||
O0908AAN0A: 'SN908',
|
||||
O0908AAV0A: 'SV908',
|
||||
O0910AAA0A: '910',
|
||||
O0913AAA0A: '913',
|
||||
O0915AAA0A: '915',
|
||||
O0917AAA0A: '917',
|
||||
O0917AAV0A: 'SV917',
|
||||
O0918AAA0A: '918',
|
||||
O0918AAP0A: 'SP918',
|
||||
O0919AAA0A: '919',
|
||||
O0919AAN0A: 'SN919',
|
||||
O0920AAA0A: '920',
|
||||
O0921AAA0A: '921',
|
||||
O0921AAV0A: 'SV921',
|
||||
O0922AAA0A: '922',
|
||||
O0922AAV0A: 'SV922',
|
||||
O0925AAA0A: '925',
|
||||
O0926AAA0A: '926',
|
||||
O0928AAA0A: '928',
|
||||
O0932AAA0A: '932',
|
||||
O0940AAA0A: '940',
|
||||
O0944AAA0A: 'LECD143',
|
||||
O0946AAA0A: '946',
|
||||
O0950AAA0A: 'LECD144',
|
||||
O0951AAA0A: 'LECD145',
|
||||
O0954AAA0A: '954',
|
||||
O0954AAN0A: 'SN954',
|
||||
O0959AAA0A: 'LECD146',
|
||||
O0961AAA0A: '961',
|
||||
O0975AAA0A: '975',
|
||||
O0979AAA0A: '979',
|
||||
O0987AAA0A: '987',
|
||||
O0990AAA0A: '990',
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { greatCircleKm } from '../geoDistance.js';
|
||||
import { RIO_ROUTE_NAMES } from './rioRouteNames.js';
|
||||
|
||||
/**
|
||||
* @module transitFeeds
|
||||
@@ -376,6 +377,18 @@ function burlingtonRouteLabel(routeId) {
|
||||
return BURLINGTON_ROUTE_NAMES[routeId] || routeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rio route key to the line a rider would say. One SMTR `routes.txt` covers
|
||||
* both feeds (see rioRouteNames.js).
|
||||
* @param {string|null} routeId
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function rioRouteLabel(routeId) {
|
||||
return Object.hasOwn(RIO_ROUTE_NAMES, routeId)
|
||||
? RIO_ROUTE_NAMES[routeId]
|
||||
: routeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of feeds. Order is presentation order in the stats/credit text.
|
||||
* `loadRadiusKm` is the distance from `center` inside which the feed is polled.
|
||||
@@ -729,6 +742,52 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
|
||||
defaultMode: 'bus',
|
||||
routeLabel: burlingtonRouteLabel,
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'smtr-rio-onibus',
|
||||
name: 'Rio buses (SPPO)',
|
||||
operator: 'Secretaria Municipal de Transportes (SMTR), Rio de Janeiro',
|
||||
region: 'Rio de Janeiro, RJ',
|
||||
// Every vehicle in a 2026-10-29 snapshot, from Santa Cruz in the west to
|
||||
// the Zona Sul, sat within 31 km of this point.
|
||||
center: Object.freeze({ lat: -22.92, lon: -43.42 }),
|
||||
loadRadiusKm: 45,
|
||||
url: 'https://dados.mobilidade.rio/gtfs/realtime/onibus/vehicle-positions',
|
||||
license: 'CC BY 4.0',
|
||||
licenseUrl:
|
||||
'https://www.data.rio/documents/997bc16cc4344a5cb0e48ab06cbdfd1f',
|
||||
attribution: 'SMTR / Prefeitura da Cidade do Rio de Janeiro — buses (SPPO)',
|
||||
defaultEnabled: true,
|
||||
terms: Object.freeze({
|
||||
quote:
|
||||
'This work is licensed under a Creative Commons Attribution 4.0 International License',
|
||||
note: 'The data.rio catalog page labels the feed [beta] and states a gateway limit of 5 requests a second and 60 a minute behind a 10-second cache, which the 15-second proxy TTL stays inside. SMTR relays the concessionaires’ ITS data and repairs trips with its own matching; the page places accuracy with the suppliers. Speeds are m/s, as the spec requires: checked against vehicle movement on 2026-11-05, though a 2026-10-29 snapshot read as km/h. Line names come from SMTR’s GTFS `routes.txt`, under the same CC BY 4.0 terms.',
|
||||
}),
|
||||
defaultMode: 'bus',
|
||||
routeLabel: rioRouteLabel,
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'smtr-rio-brt',
|
||||
name: 'Mobi-Rio BRT',
|
||||
operator:
|
||||
'Secretaria Municipal de Transportes (SMTR), Rio de Janeiro, for the Mobi-Rio BRT fleet',
|
||||
region: 'Rio de Janeiro, RJ',
|
||||
center: Object.freeze({ lat: -22.92, lon: -43.42 }),
|
||||
loadRadiusKm: 45,
|
||||
url: 'https://dados.mobilidade.rio/gtfs/realtime/brt/vehicle-positions',
|
||||
license: 'CC BY 4.0',
|
||||
licenseUrl:
|
||||
'https://www.data.rio/documents/8bb196cc02174d25b77437bb2f6443d4',
|
||||
attribution:
|
||||
'SMTR / Prefeitura da Cidade do Rio de Janeiro — BRT (Mobi-Rio)',
|
||||
defaultEnabled: true,
|
||||
terms: Object.freeze({
|
||||
quote:
|
||||
'This work is licensed under a Creative Commons Attribution 4.0 International License',
|
||||
note: 'Same catalog terms, [beta] label and gateway limits as the bus feed. The fleet is operated by Mobi-Rio and tracked by SONDA, whose data SMTR relays. Speeds are m/s too. Line names come from the same `routes.txt`.',
|
||||
}),
|
||||
defaultMode: 'bus',
|
||||
routeLabel: rioRouteLabel,
|
||||
}),
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
transitModeResolved,
|
||||
transitRouteLabel,
|
||||
} from './transitFeeds.js';
|
||||
import { RIO_ROUTE_NAMES } from './rioRouteNames.js';
|
||||
|
||||
test('every registered feed is keyless, https, licensed, and uniquely identified', () => {
|
||||
const ids = new Set();
|
||||
@@ -482,3 +483,72 @@ test('Burlington is live and carries its terms link, which its licence requires'
|
||||
assert.match(bur.licenseUrl, /Open%20Data%20Terms%20of%20Use\.pdf$/);
|
||||
assert.match(bur.url, /^https:\/\//, 'port 80 has no listener at all');
|
||||
});
|
||||
|
||||
test('Rio buses and BRT cover the city from Centro to Santa Cruz, and not São Paulo', () => {
|
||||
const ids = (lat, lon) =>
|
||||
transitFeedsInRange(lat, lon).map((feed) => feed.id);
|
||||
for (const [place, lat, lon] of [
|
||||
['Centro', -22.9068, -43.1729],
|
||||
['Barra da Tijuca', -23.0004, -43.3659],
|
||||
['Santa Cruz', -22.9194, -43.6847],
|
||||
]) {
|
||||
assert.ok(
|
||||
ids(lat, lon).includes('smtr-rio-onibus'),
|
||||
`buses reach ${place}`,
|
||||
);
|
||||
assert.ok(ids(lat, lon).includes('smtr-rio-brt'), `BRT reaches ${place}`);
|
||||
}
|
||||
assert.deepEqual(
|
||||
ids(-23.5505, -46.6333).filter((id) => id.startsWith('smtr-rio')),
|
||||
[],
|
||||
'São Paulo, 360 km away, never polls Rio',
|
||||
);
|
||||
});
|
||||
|
||||
test('Rio route keys read as the line on the front of the bus', () => {
|
||||
// Some keys look parseable and are not (`O0014AAA0A` is LECD135).
|
||||
const buses = getTransitFeed('smtr-rio-onibus');
|
||||
assert.equal(transitRouteLabel(buses, 'O0868AAA0A'), '868');
|
||||
assert.equal(transitRouteLabel(buses, 'O0731AAN0A'), 'SN731');
|
||||
assert.equal(transitRouteLabel(buses, 'O0759AAP0A'), 'SP759');
|
||||
assert.equal(transitRouteLabel(buses, 'O0014AAA0A'), 'LECD135');
|
||||
assert.equal(transitRouteLabel(buses, 'E2303AAA0A'), '2303');
|
||||
const brt = getTransitFeed('smtr-rio-brt');
|
||||
assert.equal(transitRouteLabel(brt, '20000501110'), '50');
|
||||
assert.equal(transitRouteLabel(brt, '20000EXEC1110'), 'ESP01');
|
||||
// Unknown ids, prototype names included, keep their raw key.
|
||||
assert.equal(transitRouteLabel(buses, 'O9999AAA0A'), 'O9999AAA0A');
|
||||
assert.equal(transitRouteLabel(buses, 'constructor'), 'constructor');
|
||||
assert.equal(transitRouteLabel(brt, null), null);
|
||||
});
|
||||
|
||||
test('the Rio route table is a clean join of published keys to names', () => {
|
||||
const entries = Object.entries(RIO_ROUTE_NAMES);
|
||||
assert.ok(entries.length > 400, 'the whole published table is carried');
|
||||
for (const [id, name] of entries) {
|
||||
assert.match(id, /^(?:[OE]\d{4}[A-Z0-9]{5}|2\d{4}[A-Z0-9]{4,8})$/, id);
|
||||
assert.ok(name && name === name.trim(), `${id} has a name`);
|
||||
assert.notEqual(name, id, `${id} is translated`);
|
||||
}
|
||||
});
|
||||
|
||||
test('Rio feeds are CC BY 4.0 and live', () => {
|
||||
for (const id of ['smtr-rio-onibus', 'smtr-rio-brt']) {
|
||||
const feed = getTransitFeed(id);
|
||||
assert.equal(feed.defaultEnabled, true, `${id} is live`);
|
||||
assert.equal(feed.license, 'CC BY 4.0');
|
||||
assert.match(feed.licenseUrl, /^https:\/\/www\.data\.rio\/documents\//);
|
||||
assert.match(feed.attribution, /Prefeitura da Cidade do Rio de Janeiro/);
|
||||
assert.equal(transitModeFor(feed, null), 'bus');
|
||||
}
|
||||
const sources = readFileSync(
|
||||
new URL('../../DATA_SOURCES.md', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.ok(
|
||||
sources
|
||||
.split('\n')
|
||||
.some((line) => line.startsWith('| **SMTR Rio de Janeiro')),
|
||||
'Rio has a DATA_SOURCES row',
|
||||
);
|
||||
});
|
||||
@@ -137,6 +137,7 @@ function encodeFeed({
|
||||
const position = new PbfWriter();
|
||||
position.writeFloatField(1, entity.lat);
|
||||
position.writeFloatField(2, entity.lon);
|
||||
if (entity.speed != null) position.writeFloatField(5, entity.speed);
|
||||
vehicle.writeBytesField(2, position.finish());
|
||||
if (entity.timestamp) vehicle.writeVarintField(5, entity.timestamp);
|
||||
const descriptor = new PbfWriter();
|
||||
@@ -284,6 +285,15 @@ test('the snapshot builder fills timestamps from the header it decoded', () => {
|
||||
assert.equal(snapshot.feedTimestamp, 1_700_000_000);
|
||||
});
|
||||
|
||||
test('Rio speeds pass through as the m/s they are published in', () => {
|
||||
// Checked against vehicle movement on 2026-11-05.
|
||||
const bytes = encodeFeed({
|
||||
entities: [{ id: 'moving', lat: -22.9, lon: -43.2, speed: 10 }],
|
||||
});
|
||||
const rio = buildTransitSnapshot(getTransitFeed('smtr-rio-onibus'), bytes, 1);
|
||||
assert.equal(rio.vehicles[0].speedMps, 10);
|
||||
});
|
||||
|
||||
test('conditional-request validators and gzip ride along with feed identification', () => {
|
||||
const entur = getTransitFeed('entur-norway');
|
||||
const plain = transitUpstreamHeaders(entur);
|
||||
|
||||
@@ -196,6 +196,7 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
id: cohort.id,
|
||||
label: cohort.label,
|
||||
source: cohort.source,
|
||||
sourceStatus: cohort.sourceStatus || null,
|
||||
coverage: cohort.coverage || null,
|
||||
relationship: cohort.summary.relationship,
|
||||
count: cohort.summary.count,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
t,
|
||||
translateKnown,
|
||||
} from '../../i18n/index.js';
|
||||
import { sourceStatusLabel } from '../../ui/layerNames.js';
|
||||
import { formatQuantity } from '../../ui/unitLabels.js';
|
||||
import { AWARENESS_PAGE_SIZE, AWARENESS_PAGE_ROTATE_MS } from './policy.js';
|
||||
import { distanceParts } from '../../data/distanceFormat.js';
|
||||
@@ -150,7 +151,7 @@ export function createPanel({ state: layerState, parts }) {
|
||||
relationship: summary.relationship.toLowerCase(),
|
||||
label: cohort.label,
|
||||
count: `${presentAwarenessCount(summary)}${pageLabel}`,
|
||||
detail: `${cohort.source}${coverage} · ${presentAwarenessReason(summary)}`,
|
||||
detail: `${sourceStatusLabel(cohort.source, cohort.sourceStatus)}${coverage} · ${presentAwarenessReason(summary)}`,
|
||||
nearest,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createPanel } from './panel.js';
|
||||
|
||||
test('a Contacts row names its provider as the Layers panel does, with its feed state', () => {
|
||||
const cohort = (id, source, sourceStatus) => ({
|
||||
id,
|
||||
label: id,
|
||||
source,
|
||||
sourceStatus,
|
||||
summary: { relationship: 'OBSERVED', count: 1, nearest: [], reason: 'observed' },
|
||||
});
|
||||
const state = {
|
||||
enabled: true,
|
||||
results: {
|
||||
subject: { label: 'AFR38JF' },
|
||||
cohorts: [
|
||||
cohort('flights', 'adsb.lol', 'fallback'),
|
||||
cohort('military', 'adsb.lol', null),
|
||||
cohort('ais-live-vessels', 'Datalastic', 'stale'),
|
||||
],
|
||||
},
|
||||
cohortPages: new Map(),
|
||||
panelListeners: new Set(),
|
||||
panelSignature: '',
|
||||
navigationIndex: 0,
|
||||
};
|
||||
const parts = {
|
||||
navigation: { canNavigateNext: () => false },
|
||||
rendering: { hideDirectionOverlay() {} },
|
||||
};
|
||||
createPanel({ state, parts }).renderResults();
|
||||
assert.deepEqual(
|
||||
state.panelView.rows.map((row) => row.detail),
|
||||
['adsb.lol · FALLBACK · observed', 'adsb.lol · observed', 'Datalastic · STALE · observed'],
|
||||
);
|
||||
state.offPanelLocale?.();
|
||||
});
|
||||
@@ -91,6 +91,7 @@ export function createQueries({ state: layerState, services, parts, source }) {
|
||||
stats.count ?? null,
|
||||
stats.status || null,
|
||||
stats.error || null,
|
||||
stats.source || null,
|
||||
];
|
||||
})
|
||||
.map((parts) => parts.join(':'))
|
||||
|
||||
@@ -81,3 +81,29 @@ test('Contacts excludes square corners and measures from the current moving subj
|
||||
assert.equal(moved.reason, 'mapped matches within 100 km of the subject');
|
||||
assert.deepEqual(installationState.contextAnchor, { latitude: 0, longitude: 0 });
|
||||
});
|
||||
|
||||
test('Contacts cohorts carry the provider and whether it answered as a fallback or stale', () => {
|
||||
const services = { installations: { getNearby: () => [], getStats: () => ({}) },
|
||||
flights: { getNearby: () => [] }, military: { getNearby: () => [] }, vessels: { getNearby: () => [] } };
|
||||
const parts = { navigation: { summarizeAwarenessCohortForNavigation: (items, source) =>
|
||||
summarizeAwarenessCohort(items, source) } };
|
||||
parts.queries = createQueries({ state: {}, services, parts });
|
||||
const subjectOwner = createSubject({ state: {}, services, parts });
|
||||
const states = {
|
||||
// The OpenSky proxy answers from adsb.lol when OpenSky fails.
|
||||
flights: { available: true, stats: { source: 'adsb.lol', coverage: '250nm regional fallback', count: 4, lastUpdate: 1 } },
|
||||
military: { available: true, stats: { source: 'adsb.lol', fallback: false, count: 2, lastUpdate: 1 } },
|
||||
'ais-live-vessels': { available: true, stats: { source: 'Datalastic', stale: true, count: 3, lastUpdate: 1 } },
|
||||
'military-installations': { available: true, stats: { coverage: { kind: 'subject', radiusM: 100000 } } },
|
||||
};
|
||||
const { cohorts } = subjectOwner.evaluateSubject({
|
||||
id: 'subject', layerId: 'flights', position: Cesium.Cartesian3.fromDegrees(0, 0, 10000),
|
||||
}, states);
|
||||
const byId = Object.fromEntries(cohorts.map((cohort) => [cohort.id, cohort]));
|
||||
assert.equal(byId.flights.source, 'adsb.lol');
|
||||
assert.equal(byId.flights.sourceStatus, 'fallback');
|
||||
assert.equal(byId.military.sourceStatus, null);
|
||||
assert.equal(byId['ais-live-vessels'].source, 'Datalastic');
|
||||
assert.equal(byId['ais-live-vessels'].sourceStatus, 'stale');
|
||||
assert.equal(byId['military-installations'].sourceStatus, null);
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { createQueries } from './queries.js';
|
||||
import { createSubject } from './subject.js';
|
||||
import { createModel } from './model.js';
|
||||
import { summarizeAwarenessCohort } from '../../data/militaryAwarenessEngine.js';
|
||||
import { layerFeedState } from '../../data/feedState.js';
|
||||
import aisLiveVesselsLayer, {
|
||||
_setVesselStateForTest,
|
||||
_beginAisSessionForTest,
|
||||
_applyAisFeedSnapshotForTest,
|
||||
_setAisRuntimeForTest,
|
||||
} from '../../data/aisLiveVessels.js';
|
||||
|
||||
test('a poll that returns the same stale AIS snapshot restores STALE on Contacts cohorts', () => {
|
||||
globalThis.document ??= { body: null };
|
||||
const services = {
|
||||
installations: { getNearby: () => [], getStats: () => ({}) },
|
||||
flights: { getNearby: () => [] },
|
||||
military: { getNearby: () => [] },
|
||||
vessels: { getNearby: () => [] },
|
||||
};
|
||||
const layerState = {
|
||||
enabled: true,
|
||||
passive: true,
|
||||
sourceRevision: '',
|
||||
cohortPages: new Map(),
|
||||
subject: {
|
||||
id: 'site-1',
|
||||
layerId: 'military-installations',
|
||||
label: 'Site',
|
||||
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
|
||||
},
|
||||
};
|
||||
let states;
|
||||
const parts = {
|
||||
navigation: {
|
||||
summarizeAwarenessCohortForNavigation: (items, source) =>
|
||||
summarizeAwarenessCohort(items, source),
|
||||
isFlightLayer: (id) => id === 'flights' || id === 'military',
|
||||
},
|
||||
rendering: { renderVisual() {}, scheduleDirectionOverlayUpdate() {} },
|
||||
panel: { renderResults() {} },
|
||||
};
|
||||
const realQueries = createQueries({ state: layerState, services, parts });
|
||||
parts.queries = { ...realQueries, collectSourceStates: () => states };
|
||||
parts.model = createModel({ state: layerState, services, parts });
|
||||
const subject = createSubject({ state: layerState, services, parts });
|
||||
const flights = (lastUpdate) => ({
|
||||
available: true, enabled: true, stale: false,
|
||||
stats: { source: 'OpenSky Network', count: 4, lastUpdate },
|
||||
});
|
||||
const vessels = (loading) => ({
|
||||
available: true, enabled: true, stale: true,
|
||||
stats: { source: 'Datalastic', stale: true, count: 3, lastUpdate: 1000, loading },
|
||||
});
|
||||
const rest = {
|
||||
military: { available: true, enabled: true, stale: false, stats: { source: 'adsb.lol', fallback: false, count: 1, lastUpdate: 1 } },
|
||||
'military-installations': { available: true, enabled: true, stale: false, stats: { coverage: { kind: 'subject', radiusM: 100000 } } },
|
||||
};
|
||||
const ais = () => layerState.results.cohorts.find((c) => c.id === 'ais-live-vessels');
|
||||
|
||||
// 1. Stale AIS cache, nothing loading.
|
||||
states = { flights: flights(1), 'ais-live-vessels': vessels(false), ...rest };
|
||||
subject.refreshSelectedSubject(true);
|
||||
assert.equal(ais().sourceStatus, 'stale');
|
||||
|
||||
// 2. AIS re-poll in flight while the flights poll lands (revision changes).
|
||||
states = { flights: flights(2), 'ais-live-vessels': vessels(true), ...rest };
|
||||
subject.refreshSelectedSubject();
|
||||
assert.equal(ais().sourceStatus, 'stale', 'a poll in flight keeps the snapshot stale');
|
||||
|
||||
// 3. AIS poll returns the same stale snapshot; only loading flips back.
|
||||
states = { flights: flights(2), 'ais-live-vessels': vessels(false), ...rest };
|
||||
subject.refreshSelectedSubject();
|
||||
subject.refreshSelectedSubject();
|
||||
assert.equal(ais().sourceStatus, 'stale', 'Layers reads STALE again; Contacts/Cockpit must too');
|
||||
});
|
||||
|
||||
function fakeRuntime() {
|
||||
return {
|
||||
now: () => 1000,
|
||||
setTimeout: () => ({}),
|
||||
clearTimeout() {},
|
||||
};
|
||||
}
|
||||
|
||||
test('first AIS connect (no snapshot yet) carries no STALE on the Contacts cohort', () => {
|
||||
globalThis.document ??= { body: null };
|
||||
_setAisRuntimeForTest(fakeRuntime());
|
||||
_setVesselStateForTest({ viewer: {}, records: [] });
|
||||
try {
|
||||
_beginAisSessionForTest();
|
||||
// What server/providers/vessels/ais-live.js answers while its socket connects.
|
||||
_applyAisFeedSnapshotForTest({}, {
|
||||
rows: [], source: 'AISStream', status: 'connecting', refreshing: true,
|
||||
});
|
||||
const vesselStats = aisLiveVesselsLayer.getStats();
|
||||
assert.equal(vesselStats.loading, true);
|
||||
assert.equal(vesselStats.stale, true);
|
||||
assert.equal(vesselStats.lastUpdate, null);
|
||||
assert.equal(layerFeedState(vesselStats), 'loading', 'Layers chip reads LOADING');
|
||||
|
||||
const flightsStats = { source: 'OpenSky Network', count: 2, lastUpdate: 5 };
|
||||
const militaryStats = { source: 'adsb.lol', fallback: false, count: 1, lastUpdate: 5 };
|
||||
const installStats = { coverage: { kind: 'subject', radiusM: 100000 } };
|
||||
const statsById = {
|
||||
flights: flightsStats,
|
||||
military: militaryStats,
|
||||
'ais-live-vessels': vesselStats,
|
||||
'military-installations': installStats,
|
||||
};
|
||||
const services = {
|
||||
installations: { getNearby: () => [], getStats: () => installStats },
|
||||
flights: { getNearby: () => [] },
|
||||
military: { getNearby: () => [] },
|
||||
vessels: aisLiveVesselsLayer,
|
||||
};
|
||||
const layerState = {
|
||||
enabled: true,
|
||||
passive: true,
|
||||
sourceRevision: '',
|
||||
cohortPages: new Map(),
|
||||
dataManager: {
|
||||
isEnabled: () => true,
|
||||
getLayerLifecycleState: () => ({ enabled: true, lifecycleState: 'enabled' }),
|
||||
layers: new Map(Object.entries(statsById).map(([id, s]) => [id, { module: { getStats: () => s } }])),
|
||||
},
|
||||
subject: {
|
||||
id: 'site-1',
|
||||
layerId: 'military-installations',
|
||||
label: 'Site',
|
||||
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
|
||||
},
|
||||
};
|
||||
const parts = {
|
||||
navigation: {
|
||||
summarizeAwarenessCohortForNavigation: (items, source) => summarizeAwarenessCohort(items, source),
|
||||
isFlightLayer: (id) => id === 'flights' || id === 'military',
|
||||
},
|
||||
rendering: { renderVisual() {}, scheduleDirectionOverlayUpdate() {} },
|
||||
panel: { renderResults() {} },
|
||||
};
|
||||
parts.queries = createQueries({ state: layerState, services, parts });
|
||||
parts.model = createModel({ state: layerState, services, parts });
|
||||
const subject = createSubject({ state: layerState, services, parts });
|
||||
|
||||
const vesselsSource = parts.queries.collectSourceStates()['ais-live-vessels'];
|
||||
assert.equal(vesselsSource.available, false, 'never-answered source is unavailable');
|
||||
|
||||
subject.refreshSelectedSubject(true);
|
||||
const ais = layerState.results.cohorts.find((c) => c.id === 'ais-live-vessels');
|
||||
assert.equal(ais.sourceStatus, null, 'no snapshot yet: no STALE label, Layers says LOADING');
|
||||
} finally {
|
||||
_setVesselStateForTest({ enabled: false });
|
||||
_setAisRuntimeForTest();
|
||||
}
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
AWARENESS_RELATIONSHIP,
|
||||
} from '../../data/militaryAwarenessEngine.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { layerFeedState } from '../../data/feedState.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import {
|
||||
AWARENESS_QUERY_LIMIT,
|
||||
@@ -52,6 +53,18 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
const vesselsState = sourceStates['ais-live-vessels'];
|
||||
const installationsState = sourceStates['military-installations'];
|
||||
const evaluatedAt = Date.now();
|
||||
// A poll in flight keeps FALLBACK or STALE: the revision key ignores loading.
|
||||
// Before the first answer there is no snapshot, so loading stays.
|
||||
const sourceStatus = (sourceState) => {
|
||||
const feedState = layerFeedState(
|
||||
sourceState.available
|
||||
? { ...sourceState.stats, loading: false }
|
||||
: sourceState.stats,
|
||||
);
|
||||
return feedState === 'fallback' || feedState === 'stale'
|
||||
? feedState
|
||||
: null;
|
||||
};
|
||||
const retainedProvenance = (id, label, sourceState, fallbackSource) => {
|
||||
const retainedSource = sourceState.stats.source || fallbackSource;
|
||||
return {
|
||||
@@ -94,6 +107,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
id: 'flights',
|
||||
label: t('Flights'),
|
||||
source: flightsState.stats.source || SOURCE_LABEL.flights,
|
||||
sourceStatus: sourceStatus(flightsState),
|
||||
provenance: retainedProvenance(
|
||||
'flights',
|
||||
'Flights',
|
||||
@@ -113,6 +127,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
id: 'military',
|
||||
label: t('Military flights'),
|
||||
source: militaryState.stats.source || SOURCE_LABEL.military,
|
||||
sourceStatus: sourceStatus(militaryState),
|
||||
provenance: retainedProvenance(
|
||||
'military',
|
||||
'Military flights',
|
||||
@@ -132,6 +147,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
id: 'ais-live-vessels',
|
||||
label: t('AIS vessels'),
|
||||
source: vesselsState.stats.source || SOURCE_LABEL['ais-live-vessels'],
|
||||
sourceStatus: sourceStatus(vesselsState),
|
||||
provenance: retainedProvenance(
|
||||
'ais-live-vessels',
|
||||
'AIS vessels',
|
||||
@@ -149,6 +165,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
source:
|
||||
installationsState.stats.source ||
|
||||
SOURCE_LABEL['military-installations'],
|
||||
sourceStatus: sourceStatus(installationsState),
|
||||
provenance: retainedProvenance(
|
||||
'military-installations',
|
||||
'Mapped installations',
|
||||
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
} from '../../data/transitFeeds.js';
|
||||
import { transitModeTier } from '../../data/transitPresetStyle.js';
|
||||
|
||||
/** Regions, not feeds: one region can carry several feeds (Rio has two). */
|
||||
const TRANSIT_REGION_COUNT = new Set(
|
||||
TRANSIT_ENABLED_FEEDS.map((feed) => feed.region),
|
||||
).size;
|
||||
|
||||
/**
|
||||
* The registry entry behind a vehicle. Live feeds answer first so a feed the
|
||||
* session disabled stops supplying vocabulary; the registry is the fallback,
|
||||
@@ -249,7 +254,7 @@ export function createQueries({ state, parts }) {
|
||||
// English data (the analyst's empty note to the model); the voice
|
||||
// card shows it through NOTE_PATTERNS (voice/resultDisplay.js).
|
||||
coverage: state._altitudeGateOpen
|
||||
? `No feed here yet · ${TRANSIT_ENABLED_FEEDS.length} regions available`
|
||||
? `No feed here yet · ${TRANSIT_REGION_COUNT} regions available`
|
||||
: `Fly below ${Math.round(ACTIVATION_ALTITUDE_M / 1000).toLocaleString('en-US')} km to a covered region`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createQueries } from './queries.js';
|
||||
import { TRANSIT_ENABLED_FEEDS } from '../../data/transitFeeds.js';
|
||||
|
||||
test('the empty-coverage note counts regions, not feeds', () => {
|
||||
const rio = TRANSIT_ENABLED_FEEDS.filter(
|
||||
(feed) => feed.region === 'Rio de Janeiro, RJ',
|
||||
);
|
||||
assert.equal(rio.length, 2, 'Rio is one region with two feeds');
|
||||
const regions = new Set(TRANSIT_ENABLED_FEEDS.map((feed) => feed.region));
|
||||
assert.ok(regions.size < TRANSIT_ENABLED_FEEDS.length);
|
||||
|
||||
const stats = createQueries({
|
||||
state: {
|
||||
_activeFeeds: new Map(),
|
||||
_vehicles: new Map(),
|
||||
_enabled: true,
|
||||
_altitudeGateOpen: true,
|
||||
},
|
||||
parts: {},
|
||||
}).methods.getStats();
|
||||
assert.equal(stats.status, 'zoom-in');
|
||||
assert.equal(
|
||||
stats.coverage,
|
||||
`No feed here yet · ${regions.size} regions available`,
|
||||
);
|
||||
});
|
||||
@@ -409,3 +409,15 @@ test('empty coverage cannot hide stale, incomplete or rejected source data', asy
|
||||
assert.ok(layer.getStats().error, JSON.stringify(patch));
|
||||
}
|
||||
});
|
||||
|
||||
test('vessel stats name the provider the last snapshot reported', async () => {
|
||||
const { layer } = setup({
|
||||
label: 'Vessels',
|
||||
async getSnapshot() {
|
||||
return snapshot([observation('111')], { source: 'Datalastic' });
|
||||
},
|
||||
});
|
||||
assert.equal(layer.getStats().source, 'Vessels');
|
||||
await layer.update();
|
||||
assert.equal(layer.getStats().source, 'Datalastic');
|
||||
});
|
||||
@@ -548,6 +548,8 @@ export function createQueries({
|
||||
// backing off, say how long until the next attempt instead of leaving
|
||||
// the user to guess whether anything is still happening.
|
||||
retryInSec: aisRetryInSec(),
|
||||
// Same field as flights and military, so Contacts names the provider.
|
||||
source: layer.source,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Contact readouts and nearby-signal presentation for the Cockpit controller. */
|
||||
import * as Cesium from 'cesium';
|
||||
import { formatNumber, t, tn } from '../i18n/index.js';
|
||||
import { sourceStatusLabel } from './layerNames.js';
|
||||
import { unitLabel } from './unitLabels.js';
|
||||
import {
|
||||
bearingBetweenCoordinates,
|
||||
@@ -110,6 +111,7 @@ export function updateContext(info, heading) {
|
||||
if (shown) {
|
||||
shown.count = cohort.count === null ? '?' : String(cohort.count);
|
||||
shown.unknown = cohort.relationship === 'UNKNOWN';
|
||||
shown.source = sourceStatusLabel(cohort.source, cohort.sourceStatus);
|
||||
}
|
||||
if (cohort.count === null) unknownCount += 1;
|
||||
for (const item of cohort.nearest) nearest.push({ ...item, cohort });
|
||||
|
||||
@@ -96,6 +96,13 @@ test('before the controller presents, the card shows the state it starts in', (t
|
||||
),
|
||||
['—', '—', '—', '—'],
|
||||
);
|
||||
// No provider is named before a snapshot reports one.
|
||||
assert.deepEqual(
|
||||
[...document.querySelectorAll('[data-context-cohort] small')].map(
|
||||
(source) => source.textContent,
|
||||
),
|
||||
['—', '—', '—', '—'],
|
||||
);
|
||||
assert.equal(text('#cockpit-context-nearest-label'), 'NO AVAILABLE EXAMPLE');
|
||||
assert.equal(
|
||||
$('#cockpit-context-nearest-label').hasAttribute('aria-label'),
|
||||
@@ -159,6 +166,43 @@ test('a snapshot fills the card and keeps classes other code sets', async (t) =>
|
||||
assert.equal(layouts.length, 1);
|
||||
});
|
||||
|
||||
test('each count names the provider its snapshot reports, with its feed state', async (t) => {
|
||||
const reported = snapshot();
|
||||
reported.cohorts[0].source = 'adsb.lol';
|
||||
reported.cohorts[0].sourceStatus = 'fallback';
|
||||
reported.cohorts[1].source = 'Plane Finder';
|
||||
reported.cohorts.push(
|
||||
{
|
||||
id: 'ais-live-vessels',
|
||||
label: 'AIS vessels',
|
||||
source: 'Datalastic',
|
||||
sourceStatus: 'stale',
|
||||
count: 3,
|
||||
relationship: 'OBSERVED',
|
||||
nearest: [],
|
||||
},
|
||||
{
|
||||
id: 'military-installations',
|
||||
label: 'Mapped installations',
|
||||
source: 'OpenStreetMap',
|
||||
sourceStatus: null,
|
||||
count: 0,
|
||||
relationship: 'MAPPED',
|
||||
nearest: [],
|
||||
},
|
||||
);
|
||||
const { $, owner } = fixture(t, reported);
|
||||
updateContext.call(owner, INFO, 0);
|
||||
await nextTick();
|
||||
const source = (id) => $(`[data-context-cohort="${id}"] small`);
|
||||
assert.equal(source('flights').textContent, 'adsb.lol · FALLBACK');
|
||||
assert.equal(source('military').textContent, 'Plane Finder');
|
||||
assert.equal(source('ais-live-vessels').textContent, 'Datalastic · STALE');
|
||||
assert.equal(source('military-installations').textContent, 'OpenStreetMap');
|
||||
// The tile ellipsizes; the full name stays readable on hover.
|
||||
assert.equal(source('flights').title, 'adsb.lol · FALLBACK');
|
||||
});
|
||||
|
||||
test('a lost contact holds its readout behind the CONTACT LOST cue', async (t) => {
|
||||
const { $, text, owner, layouts } = fixture(t, snapshot());
|
||||
updateContext.call(owner, INFO, 0);
|
||||
|
||||
@@ -25,7 +25,10 @@ export function createContextState() {
|
||||
canPrevious: true,
|
||||
canNext: true,
|
||||
cohorts: Object.fromEntries(
|
||||
COCKPIT_CONTEXT_COHORTS.map((id) => [id, { count: '—', unknown: false }]),
|
||||
COCKPIT_CONTEXT_COHORTS.map((id) => [
|
||||
id,
|
||||
{ count: '—', unknown: false, source: '—' },
|
||||
]),
|
||||
),
|
||||
nearest: { label: t('NO AVAILABLE EXAMPLE'), ariaLabel: null },
|
||||
distance: { text: '—', ariaLabel: null },
|
||||
|
||||
@@ -141,3 +141,17 @@ export function sourceLabel(source) {
|
||||
? source.split(' · ').map(translateKnown).join(' · ')
|
||||
: translateKnown(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* A provider name with the feed state it answered in, so a fallback or stale
|
||||
* snapshot does not read as the live primary feed.
|
||||
* @param {string} source English source name.
|
||||
* @param {'fallback'|'stale'|null} status Feed state from `layerFeedState`.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function sourceStatusLabel(source, status) {
|
||||
const name = sourceLabel(source);
|
||||
if (status === 'fallback') return `${name} · ${t('FALLBACK')}`;
|
||||
if (status === 'stale') return `${name} · ${t('STALE')}`;
|
||||
return name;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
LAYER_NAME_KEYS,
|
||||
PANEL_LABEL_KEYS,
|
||||
sourceLabel,
|
||||
sourceStatusLabel,
|
||||
} from './layerNames.js';
|
||||
|
||||
// Layers whose module name is a proper name and stays as it is in every
|
||||
@@ -51,3 +52,19 @@ for (const locale of ['es', 'ru']) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('a provider name carries the fallback or stale state it answered in', () => {
|
||||
assert.equal(sourceStatusLabel('Plane Finder', null), 'Plane Finder');
|
||||
assert.equal(sourceStatusLabel('adsb.lol', 'fallback'), 'adsb.lol · FALLBACK');
|
||||
assert.equal(sourceStatusLabel('OpenSky Network', 'stale'), 'OpenSky Network · STALE');
|
||||
setLocale('ru');
|
||||
try {
|
||||
assert.equal(
|
||||
sourceStatusLabel('adsb.lol', 'fallback'),
|
||||
`adsb.lol · ${t('FALLBACK')}`,
|
||||
);
|
||||
assert.notEqual(t('FALLBACK'), 'FALLBACK');
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
@@ -49,10 +49,10 @@ const weatherLabel = computed(() =>
|
||||
<span class="cockpit-context-qualifier">{{ t('CONTEXT ONLY') }}</span>
|
||||
</header>
|
||||
<div class="cockpit-context-cohorts" role="list" :aria-label="t('Nearby cohort counts')">
|
||||
<div v-class-toggle="{ unknown: c.cohorts.flights.unknown }" role="listitem" data-context-cohort="flights"><span>{{ t('FLT') }}</span><strong>{{ c.cohorts.flights.count }}</strong><small translate="no">OpenSky</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts.military.unknown }" role="listitem" data-context-cohort="military"><span>{{ t('MIL') }}</span><strong>{{ c.cohorts.military.count }}</strong><small translate="no">adsb.lol</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts['ais-live-vessels'].unknown }" role="listitem" data-context-cohort="ais-live-vessels"><span translate="no">AIS</span><strong>{{ c.cohorts['ais-live-vessels'].count }}</strong><small translate="no">AISStream</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts['military-installations'].unknown }" role="listitem" data-context-cohort="military-installations"><span>{{ t('SITE') }}</span><strong>{{ c.cohorts['military-installations'].count }}</strong><small translate="no">OpenStreetMap</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts.flights.unknown }" role="listitem" data-context-cohort="flights"><span>{{ t('FLT') }}</span><strong>{{ c.cohorts.flights.count }}</strong><small translate="no" :title="c.cohorts.flights.source">{{ c.cohorts.flights.source }}</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts.military.unknown }" role="listitem" data-context-cohort="military"><span>{{ t('MIL') }}</span><strong>{{ c.cohorts.military.count }}</strong><small translate="no" :title="c.cohorts.military.source">{{ c.cohorts.military.source }}</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts['ais-live-vessels'].unknown }" role="listitem" data-context-cohort="ais-live-vessels"><span translate="no">AIS</span><strong>{{ c.cohorts['ais-live-vessels'].count }}</strong><small translate="no" :title="c.cohorts['ais-live-vessels'].source">{{ c.cohorts['ais-live-vessels'].source }}</small></div>
|
||||
<div v-class-toggle="{ unknown: c.cohorts['military-installations'].unknown }" role="listitem" data-context-cohort="military-installations"><span>{{ t('SITE') }}</span><strong>{{ c.cohorts['military-installations'].count }}</strong><small translate="no" :title="c.cohorts['military-installations'].source">{{ c.cohorts['military-installations'].source }}</small></div>
|
||||
</div>
|
||||
<div class="cockpit-context-nearest">
|
||||
<span id="cockpit-context-direction" v-class-toggle="{ unknown: c.direction.unknown }" class="material-symbols-outlined" :style="directionStyle" aria-hidden="true">navigation</span>
|
||||
|
||||
Reference in new issue
Block a user