@@ -6,19 +6,28 @@ import {
66 Tooltip ,
77 Skeleton ,
88 Button ,
9+ Typography ,
910} from '@mui/material' ;
1011import MapIcon from '@mui/icons-material/Map' ;
1112import TravelExploreIcon from '@mui/icons-material/TravelExplore' ;
1213import { ContentBox } from './ContentBox' ;
1314import { WarningContentBox } from './WarningContentBox' ;
1415import { mapBoxPositionStyle } from '../screens/Feed/Feed.styles' ;
15- import { type GeoJSONData , MapGeoJSON } from './MapGeoJSON' ;
16+ import {
17+ type GeoJSONData ,
18+ type GeoJSONDataGBFS ,
19+ MapGeoJSON ,
20+ } from './MapGeoJSON' ;
1621import { Map } from './Map' ;
1722import { useTranslation } from 'react-i18next' ;
1823import type { LatLngExpression } from 'leaflet' ;
1924import { useTheme } from '@mui/material/styles' ;
2025import { type GBFSFeedType , type AllFeedType } from '../services/feeds/utils' ;
2126import { OpenInNew } from '@mui/icons-material' ;
27+ import { computeBoundingBox } from '../screens/Feed/Feed.functions' ;
28+ import { displayFormattedDate } from '../utils/date' ;
29+ import { useSelector } from 'react-redux' ;
30+ import { selectLatestGbfsVersion } from '../store/feed-selectors' ;
2231
2332interface CoveredAreaMapProps {
2433 boundingBox ?: LatLngExpression [ ] ;
@@ -28,7 +37,7 @@ interface CoveredAreaMapProps {
2837
2938export const fetchGeoJson = async (
3039 latestDatasetUrl : string ,
31- ) : Promise < GeoJSONData > => {
40+ ) : Promise < GeoJSONData | GeoJSONDataGBFS > => {
3241 const geoJsonUrl = latestDatasetUrl
3342 . split ( '/' )
3443 . slice ( 0 , - 2 )
@@ -49,36 +58,57 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
4958 const { t } = useTranslation ( 'feeds' ) ;
5059 const theme = useTheme ( ) ;
5160
52- const [ geoJsonData , setGeoJsonData ] = useState < GeoJSONData | null > ( null ) ;
61+ const [ geoJsonData , setGeoJsonData ] = useState <
62+ GeoJSONData | GeoJSONDataGBFS | null
63+ > ( null ) ;
5364 const [ geoJsonError , setGeoJsonError ] = useState ( false ) ;
5465 const [ geoJsonLoading , setGeoJsonLoading ] = useState ( false ) ;
5566 const [ view , setView ] = useState <
5667 'boundingBoxView' | 'detailedCoveredAreaView'
5768 > ( 'detailedCoveredAreaView' ) ;
69+ const latestGbfsVersion = useSelector ( selectLatestGbfsVersion ) ;
70+
71+ const getAndSetGeoJsonData = ( urlToExtract : string ) : void => {
72+ setGeoJsonLoading ( true ) ;
73+ fetchGeoJson ( urlToExtract )
74+ . then ( ( data ) => {
75+ setGeoJsonData ( data ) ;
76+ setGeoJsonError ( false ) ;
77+ setView ( 'detailedCoveredAreaView' ) ;
78+ } )
79+ . catch ( ( ) => {
80+ setGeoJsonError ( true ) ;
81+ setView ( 'boundingBoxView' ) ;
82+ } )
83+ . finally ( ( ) => {
84+ setGeoJsonLoading ( false ) ;
85+ } ) ;
86+ } ;
5887
5988 useEffect ( ( ) => {
60- if ( latestDataset ?. hosted_url !== undefined && boundingBox != undefined ) {
61- setGeoJsonLoading ( true ) ;
62- fetchGeoJson ( latestDataset . hosted_url )
63- . then ( ( data ) => {
64- setGeoJsonData ( data ) ;
65- setGeoJsonError ( false ) ;
66- setView ( 'detailedCoveredAreaView' ) ;
67- } )
68- . catch ( ( ) => {
69- setGeoJsonError ( true ) ;
70- setView ( 'boundingBoxView' ) ;
71- } )
72- . finally ( ( ) => {
73- setGeoJsonLoading ( false ) ;
74- } ) ;
75- } else {
76- // No dataset, fallback to bounding box
77- setGeoJsonData ( null ) ;
78- setGeoJsonError ( true ) ;
79- setView ( 'boundingBoxView' ) ;
89+ if ( feed ?. data_type === 'gbfs' ) {
90+ const latestGbfsVersionReportUrl =
91+ latestGbfsVersion ?. latest_validation_report ?. report_summary_url ;
92+ if ( latestGbfsVersionReportUrl === undefined ) {
93+ setGeoJsonData ( null ) ;
94+ setGeoJsonError ( true ) ;
95+ return ;
96+ }
97+ getAndSetGeoJsonData ( latestGbfsVersionReportUrl ) ;
98+ return ;
8099 }
81- } , [ latestDataset ] ) ;
100+ if (
101+ feed ?. data_type === 'gtfs' &&
102+ latestDataset ?. hosted_url != undefined &&
103+ boundingBox != undefined
104+ ) {
105+ getAndSetGeoJsonData ( latestDataset . hosted_url ) ;
106+ return ;
107+ }
108+ setGeoJsonData ( null ) ;
109+ setGeoJsonError ( true ) ;
110+ setView ( 'boundingBoxView' ) ;
111+ } , [ latestDataset , feed ] ) ;
82112
83113 const handleViewChange = (
84114 _ : React . MouseEvent < HTMLElement > ,
@@ -87,11 +117,47 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
87117 if ( newView !== null ) setView ( newView ) ;
88118 } ;
89119
90- const getGbfsLatestVersionVisualizationUrl = ( feed : GBFSFeedType ) : string => {
91- // TODO: Redo logic when versions all have the auto discovery
92- return `https://gbfs-validator.mobilitydata.org/visualization?url=${ feed ?. source_info ?. producer_url } ` ;
120+ const getGbfsLatestVersionVisualizationUrl = (
121+ feed : GBFSFeedType ,
122+ ) : string | undefined => {
123+ const latestAutodiscoveryUrl = latestGbfsVersion ?. endpoints ?. find (
124+ ( endpoint ) => endpoint . name === 'gbfs' ,
125+ ) ?. url ;
126+ if ( latestAutodiscoveryUrl != undefined ) {
127+ return `https://gbfs-validator.mobilitydata.org/visualization?url=${ latestAutodiscoveryUrl } ` ;
128+ }
129+ return undefined ;
93130 } ;
94131
132+ const renderMap = ( ) : JSX . Element => {
133+ const displayBoundingBoxMap =
134+ view === 'boundingBoxView' && feed ?. data_type === 'gtfs' ;
135+ let gbfsBoundingBox : LatLngExpression [ ] = [ ] ;
136+ if ( feed ?. data_type === 'gbfs' ) {
137+ gbfsBoundingBox = computeBoundingBox ( geoJsonData ) ?? [ ] ;
138+ if ( gbfsBoundingBox . length === 0 ) {
139+ setGeoJsonError ( true ) ;
140+ }
141+ }
142+ return (
143+ < >
144+ { displayBoundingBoxMap ? (
145+ < Map polygon = { boundingBox ?? [ ] } />
146+ ) : (
147+ < MapGeoJSON
148+ geoJSONData = { geoJsonData }
149+ polygon = { boundingBox ?? gbfsBoundingBox }
150+ displayMapDetails = { feed ?. data_type === 'gtfs' }
151+ />
152+ ) }
153+ </ >
154+ ) ;
155+ } ;
156+
157+ const mapDisplayError = boundingBox == undefined && geoJsonError ;
158+ const latestAutodiscoveryUrl = getGbfsLatestVersionVisualizationUrl (
159+ feed as GBFSFeedType ,
160+ ) ;
95161 return (
96162 < ContentBox
97163 sx = { {
@@ -102,22 +168,39 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
102168 xs : '100%' ,
103169 md : '70vh' ,
104170 } ,
171+ minHeight : '50vh' ,
105172 } }
106- title = { t ( 'coveredAreaTitle' ) + ' - ' + t ( view ) }
173+ title = { mapDisplayError ? '' : t ( 'coveredAreaTitle' ) + ' - ' + t ( view ) }
107174 width = { { xs : '100%' } }
108175 outlineColor = { theme . palette . primary . dark }
109176 padding = { 2 }
110177 action = {
111178 < >
112179 { feed ?. data_type === 'gbfs' ? (
113- < Button
114- href = { getGbfsLatestVersionVisualizationUrl ( feed as GBFSFeedType ) }
115- target = '_blank'
116- rel = 'noreferrer'
117- endIcon = { < OpenInNew /> }
118- >
119- { t ( 'viewRealtimeVisualization' ) }
120- </ Button >
180+ < Box sx = { { textAlign : 'right' } } >
181+ { latestAutodiscoveryUrl != undefined && (
182+ < Button
183+ href = { latestAutodiscoveryUrl }
184+ target = '_blank'
185+ rel = 'noreferrer'
186+ endIcon = { < OpenInNew /> }
187+ >
188+ { t ( 'viewRealtimeVisualization' ) }
189+ </ Button >
190+ ) }
191+ { ( geoJsonData as GeoJSONDataGBFS ) ?. extracted_at != undefined && (
192+ < Typography
193+ variant = 'caption'
194+ color = 'text.secondary'
195+ sx = { { display : 'block' , px : 1 } }
196+ >
197+ { t ( 'common:updated' ) } :{ ' ' }
198+ { displayFormattedDate (
199+ ( geoJsonData as GeoJSONDataGBFS ) . extracted_at ,
200+ ) }
201+ </ Typography >
202+ ) }
203+ </ Box >
121204 ) : (
122205 < ToggleButtonGroup
123206 value = { view }
@@ -150,13 +233,19 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
150233 </ >
151234 }
152235 >
153- { boundingBox === undefined && view === 'boundingBoxView' && (
154- < WarningContentBox >
155- { t ( 'unableToGenerateBoundingBox' ) }
156- </ WarningContentBox >
236+ { feed ?. data_type === 'gtfs' &&
237+ boundingBox === undefined &&
238+ view === 'boundingBoxView' && (
239+ < WarningContentBox >
240+ { t ( 'unableToGenerateBoundingBox' ) }
241+ </ WarningContentBox >
242+ ) }
243+
244+ { feed ?. data_type === 'gbfs' && geoJsonError && (
245+ < WarningContentBox > { t ( 'unableToGetGbfsMap' ) } </ WarningContentBox >
157246 ) }
158247
159- { boundingBox !== undefined && (
248+ { ( boundingBox != undefined || ! geoJsonError ) && (
160249 < Box sx = { mapBoxPositionStyle } >
161250 { geoJsonLoading ? (
162251 < Skeleton
@@ -165,10 +254,8 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
165254 height = '100%'
166255 animation = 'wave'
167256 />
168- ) : view === 'boundingBoxView' ? (
169- < Map polygon = { boundingBox } />
170257 ) : (
171- < MapGeoJSON geoJSONData = { geoJsonData } polygon = { boundingBox } />
258+ < > { geoJsonData !== null && < > { renderMap ( ) } </ > } < />
172259 ) }
173260 </ Box >
174261 ) }
0 commit comments