From 11d2b4861b2d67ad70771d8d78cf64fcf4e7dd82 Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Tue, 17 Jun 2025 13:11:34 -0400 Subject: [PATCH 01/16] initiation of gtfs visualizations --- web-app/package.json | 4 + web-app/src/app/components/CoveredAreaMap.tsx | 67 +- .../app/components/GtfsVisualizationMap.tsx | 330 ++ web-app/src/app/components/MapElement.tsx | 104 + web-app/src/app/components/RouteSelector.tsx | 100 + .../app/components/sample-route-output.json | 3410 +++++++++++++++++ web-app/src/app/router/Router.tsx | 3 + web-app/src/app/screens/Feed/Feed.styles.ts | 6 +- .../screens/Feed/components/FeedSummary.tsx | 28 +- .../screens/Feed/components/FullMapView.tsx | 48 + .../Feed/components/sample-route-output.json | 3410 +++++++++++++++++ web-app/yarn.lock | 427 ++- 12 files changed, 7912 insertions(+), 25 deletions(-) create mode 100644 web-app/src/app/components/GtfsVisualizationMap.tsx create mode 100644 web-app/src/app/components/MapElement.tsx create mode 100644 web-app/src/app/components/RouteSelector.tsx create mode 100644 web-app/src/app/components/sample-route-output.json create mode 100644 web-app/src/app/screens/Feed/components/FullMapView.tsx create mode 100644 web-app/src/app/screens/Feed/components/sample-route-output.json diff --git a/web-app/package.json b/web-app/package.json index 154bfb146..2b6558acb 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -13,6 +13,7 @@ "@turf/center": "^6.5.0", "@types/i18next": "^13.0.0", "@types/leaflet": "^1.9.12", + "@types/react-map-gl": "^6.1.7", "axios": "^1.7.2", "countries-list": "^3.1.1", "date-fns": "^2.30.0", @@ -24,10 +25,12 @@ "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^2.5.2", "leaflet": "^1.9.4", + "maplibre-gl": "^4.7.1", "material-react-table": "^2.13.0", "mui-datatables": "^4.3.0", "mui-nested-menu": "^3.4.0", "openapi-fetch": "^0.9.3", + "pmtiles": "^4.2.1", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0", "react-ga4": "^2.1.0", @@ -36,6 +39,7 @@ "react-hook-form": "^7.52.1", "react-i18next": "^14.1.2", "react-leaflet": "^4.2.1", + "react-map-gl": "^7.1.7", "react-redux": "^8.1.3", "react-router-dom": "^6.16.0", "react-scripts": "5.0.1", diff --git a/web-app/src/app/components/CoveredAreaMap.tsx b/web-app/src/app/components/CoveredAreaMap.tsx index 343a489d2..cf2417d85 100644 --- a/web-app/src/app/components/CoveredAreaMap.tsx +++ b/web-app/src/app/components/CoveredAreaMap.tsx @@ -7,7 +7,9 @@ import { Skeleton, Button, Typography, + Fab, } from '@mui/material'; +import { Link } from 'react-router-dom'; import MapIcon from '@mui/icons-material/Map'; import TravelExploreIcon from '@mui/icons-material/TravelExplore'; import { ContentBox } from './ContentBox'; @@ -28,6 +30,9 @@ import { computeBoundingBox } from '../screens/Feed/Feed.functions'; import { displayFormattedDate } from '../utils/date'; import { useSelector } from 'react-redux'; import { selectAutodiscoveryGbfsVersion } from '../store/feed-selectors'; +import ModeOfTravelIcon from '@mui/icons-material/ModeOfTravel'; +import { GtfsVisualizationMap } from './GtfsVisualizationMap'; +import OpenInFullIcon from '@mui/icons-material/OpenInFull'; interface CoveredAreaMapProps { boundingBox?: LatLngExpression[]; @@ -50,6 +55,11 @@ export const fetchGeoJson = async ( return await response.json(); }; +type MapViews = + | 'boundingBoxView' + | 'detailedCoveredAreaView' + | 'gtfsVisualizationView'; + const CoveredAreaMap: React.FC = ({ boundingBox, latestDataset, @@ -63,9 +73,7 @@ const CoveredAreaMap: React.FC = ({ >(null); const [geoJsonError, setGeoJsonError] = useState(false); const [geoJsonLoading, setGeoJsonLoading] = useState(false); - const [view, setView] = useState< - 'boundingBoxView' | 'detailedCoveredAreaView' - >('detailedCoveredAreaView'); + const [view, setView] = useState('detailedCoveredAreaView'); const latestGbfsVersion = useSelector(selectAutodiscoveryGbfsVersion); const getAndSetGeoJsonData = (urlToExtract: string): void => { @@ -74,7 +82,7 @@ const CoveredAreaMap: React.FC = ({ .then((data) => { setGeoJsonData(data); setGeoJsonError(false); - setView('detailedCoveredAreaView'); + setView('gtfsVisualizationView'); }) .catch(() => { setGeoJsonError(true); @@ -112,7 +120,7 @@ const CoveredAreaMap: React.FC = ({ const handleViewChange = ( _: React.MouseEvent, - newView: 'boundingBoxView' | 'detailedCoveredAreaView' | null, + newView: MapViews | null, ): void => { if (newView !== null) setView(newView); }; @@ -132,6 +140,9 @@ const CoveredAreaMap: React.FC = ({ const renderMap = (): JSX.Element => { const displayBoundingBoxMap = view === 'boundingBoxView' && feed?.data_type === 'gtfs'; + + const displayGtfsVisualizationView = + view === 'gtfsVisualizationView' && feed?.data_type === 'gtfs'; let gbfsBoundingBox: LatLngExpression[] = []; if (feed?.data_type === 'gbfs') { gbfsBoundingBox = computeBoundingBox(geoJsonData) ?? []; @@ -139,18 +150,29 @@ const CoveredAreaMap: React.FC = ({ setGeoJsonError(true); } } + + if (displayBoundingBoxMap) { + return ; + } + + if (displayGtfsVisualizationView) { + return ( + <> + + + + + + + ) + } + return ( - <> - {displayBoundingBoxMap ? ( - - ) : ( - - )} - + ); }; @@ -166,7 +188,7 @@ const CoveredAreaMap: React.FC = ({ flexDirection: 'column', maxHeight: { xs: '100%', - md: '70vh', + md: '70vh', // TODO: optimize this }, minHeight: '50vh', }} @@ -176,6 +198,9 @@ const CoveredAreaMap: React.FC = ({ padding={2} action={ <> + {view === 'gtfsVisualizationView' && ( + + )} {feed?.data_type === 'gbfs' ? ( {latestAutodiscoveryUrl != undefined && ( @@ -209,6 +234,14 @@ const CoveredAreaMap: React.FC = ({ aria-label='map view selection' onChange={handleViewChange} > + + + + + { + const theme = useTheme(); + const [hoverInfo, setHoverInfo] = useState([]); + const [hoverData, setHoverData] = useState(''); + const [mapElement, setMapElement] = useState([]); + const mapRef = useRef(null); + + const handleMouseMove = (event: maplibregl.MapLayerMouseEvent): void => { + // Ensure that the mapRef is not null before trying to access the map + const map = mapRef.current?.getMap(); + const mapElements: MapElement[] = []; + + if (map != undefined) { + // Get the features under the mouse pointer + const features = map.queryRenderedFeatures(event.point, { + layers: ['stops', 'routes', 'routes-white'], // Change this to your actual layer ID + }); + + if (features.length > 0) { + features.forEach((feature) => { + if (feature.layer.id === 'stops') { + const mapElement: MapElement = { + isStop: true, + name: feature.properties.stop_name, + }; + mapElements.push(mapElement); + } else { + const mapElement: MapElement = { + isStop: false, + name: feature.properties.route_long_name, + routeType: feature.properties.route_type, + routeColor: feature.properties.route_color, + routeId: feature.properties.route_id, + }; + mapElements.push(mapElement); + } + }); + + setMapElement(mapElements); + + const elementIds: string[] = []; + features.forEach((feature) => { + if (feature.properties.route_id != undefined) { + elementIds.push(feature.properties.route_id); + } else { + elementIds.push(feature.properties.stop_id); + } + }); + setHoverInfo(elementIds); + } else { + setHoverInfo([]); + setHoverData(''); + setMapElement([]); + } + } + }; + + useEffect(() => { + // Will be called on add statup only once + const protocol = new Protocol(); + maplibregl.addProtocol('pmtiles', protocol.tile); + return () => { + maplibregl.removeProtocol('pmtiles'); + }; + }, []); + + const getBoundsFromCoordinates = ( + coordinates: [number, number][], + ): LngLatBoundsLike => { + let minLng = Number.POSITIVE_INFINITY; + let minLat = Number.POSITIVE_INFINITY; + let maxLng = Number.NEGATIVE_INFINITY; + let maxLat = Number.NEGATIVE_INFINITY; + + coordinates.forEach(([lat, lng]) => { + minLat = Math.min(minLat, lat); + maxLat = Math.max(maxLat, lat); + minLng = Math.min(minLng, lng); + maxLng = Math.max(maxLng, lng); + }); + + return [minLng, minLat, maxLng, maxLat]; // Matches LngLatBoundsLike format + }; + + const bounds: LngLatBoundsLike = getBoundsFromCoordinates( + polygon as [number, number][], + ); + + // in polygon: IMPORTANT - the coordinates are in the format [lat, lng] (not [lng, lat]) + const boundingBoxFormmated = polygon.map((point: any) => [ + point[1], + point[0], + ]); + + const geojson: FeatureCollection = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [boundingBoxFormmated], + }, + }, + ], + }; + + return ( + + + {/* setFilteredRoutes(val)}> */} + + + handleMouseMove(event)} + style={{ width: '100%', height: '100%' }} + initialViewState={{ bounds }} + interactiveLayerIds={['stops', 'routes', 'routes-white']} + scrollZoom={true} + dragPan={true} + // https://pmtiles.io/ Good tool for debugging PMTiles + mapStyle={{ + version: 8, + sources: { + 'raster-tiles': { + type: 'raster', + tiles: [ + 'https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', + ], + tileSize: 256, + attribution: + '© OpenStreetMap contributors', + }, + sample: { + type: 'vector', + url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/stops-v2.pmtiles', // Google Storage Bucket (CORS enabled) + }, + routes: { + type: 'vector', + url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/routes-v2.pmtiles', // Google Storage Bucket (CORS enabled) + }, + // boundingBox: { + // type: 'geojson', + // data: geojson, // displays the bounding box + // }, + }, + // Order matters: the last layer will be on top + layers: [ + { + id: 'simple-tiles', + type: 'raster', + source: 'raster-tiles', + minzoom: 0, + maxzoom: 22, + }, + + { + id: 'routes-white', + source: 'routes', + 'source-layer': 'routesoutput', // Name of the geojson file when converting to pmtile. route-output.geojson -> routesoutput + type: 'line', + paint: { + // style and color of the stops + 'line-color': '#ffffff', + 'line-width': [ + 'match', + ['get', 'route_type'], // Property from the vector tile + '3', + 4, // 3 = bus + '1', + 15, // 1 = metro + 3, // Default width + ], + }, + }, + { + id: 'routes', + source: 'routes', + 'source-layer': 'routesoutput', // Name of the geojson file when converting to pmtile. route-output.geojson -> routesoutput + type: 'line', + paint: { + // style and color of the stops + 'line-color': ['concat', '#', ['get', 'route_color']], + 'line-width': [ + 'match', + ['get', 'route_type'], // Property from the vector tile + '3', + 1, // 3 = bus + '1', + 4, // 1 = metro + 3, // Default width + ], + }, + layout: { + 'line-sort-key': [ + 'match', + ['get', 'route_type'], + '1', + 3, // metro on top + '3', + 2, // bus second + 0, // Default priority + ], + }, + }, + + // { + // id: 'boundingBox', + // type: 'fill', + // source: 'boundingBox', + // paint: { + // 'fill-color': '#088', + // 'fill-opacity': 0.8, + // }, + // }, + { + id: 'stops', + source: 'sample', + 'source-layer': 'stopsoutput', // Name of the geojson file when converting to pmtile. stops-output.geojson -> stopssoutput + type: 'circle', + paint: { + // style and color of the stops + 'circle-radius': 3, + 'circle-color': '#000000', + }, + minzoom: 12, + maxzoom: 22, + }, + { + id: 'routes-highlight', + source: 'routes', + 'source-layer': 'routesoutput', // Name of the geojson file when converting to pmtile. stops-output.geojson -> stopssoutput + type: 'line', + paint: { + // style and color of the stops + 'line-color': ['concat', '#', ['get', 'route_color']], + 'line-width': [ + 'match', + ['get', 'route_type'], // Property from the vector tile + '3', + 5, // 3 = bus + '1', + 6, // 1 = metro + 3, // Default width + ], + }, + minzoom: 10, + maxzoom: 22, + filter: [ + 'any', + ['in', ['get', 'route_id'], ['literal', hoverInfo]], + ['in', ['get', 'route_id'], ['literal', filteredRoutes]], + ], + }, + { + id: 'stops-highlight', + source: 'sample', + 'source-layer': 'stopsoutput', // Name of the geojson file when converting to pmtile. stops-output.geojson -> stopssoutput + type: 'circle', + paint: { + 'circle-radius': 6, + 'circle-color': [ + 'concat', + '#', + ['at', 0, ['get', 'route_colors']], + ], // Red highlight + 'circle-opacity': 0.8, + }, + filter: [ + 'any', + ['in', ['get', 'stop_id'], ['literal', hoverInfo]], + //['in', 2, ['get', 'route_ids']] + [ + 'any', + ['in', '141', ['get', 'route_ids']], + ...filteredRoutes.map((id) => { + const a = ['in', Number(id), ['get', 'route_ids']]; + return a as any; + }), + ], + [ + 'any', + ['in', '141', ['get', 'route_ids']], + ...hoverInfo.map((id) => { + const a = ['in', Number(id), ['get', 'route_ids']]; + return a as any; + }), + ], + ], + }, + ], + }} + > + + + + ); +}; diff --git a/web-app/src/app/components/MapElement.tsx b/web-app/src/app/components/MapElement.tsx new file mode 100644 index 000000000..dbe62c2c6 --- /dev/null +++ b/web-app/src/app/components/MapElement.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import 'leaflet/dist/leaflet.css'; +import { Box, Typography } from '@mui/material'; +import SubwayIcon from '@mui/icons-material/Subway'; +import DirectionsBusIcon from '@mui/icons-material/DirectionsBus'; + +export interface MapElement { + isStop: boolean; + routeType?: number; + name: string; + routeColor?: string; + routeId?: string; +} + +export interface MapElementProps { + mapElements: MapElement[]; +} + +export const MapElement = ( + props: React.PropsWithChildren, +): JSX.Element => { + + const formatSet = new Set(); + const formattedElements: MapElement[] = []; + // fE2 is not being used - to be completed + const fE2: {[key: string]: MapElement[]}= { + ['stops']: [], + ['routes']: [] + } + props.mapElements.forEach((element) => { + if(!formatSet.has(element.name)) { + formattedElements.push(element) + if(element.isStop) { + fE2['stops'].push(element) + } else { + fE2['routes'].push(element) + } + } + formatSet.add(element.name) + }); + + + + return ( + + {formattedElements.map((element, index) => { + return ( + + {element.isStop ? "Stop" :"Route"} + {element.isStop ? ( + + {element.name} + + ) : ( + + {element.routeType == 1 ? ( + + ) : ( + + )} + + {element.routeId} - {element.name} + + + )} + + ); + })} + + ); +}; \ No newline at end of file diff --git a/web-app/src/app/components/RouteSelector.tsx b/web-app/src/app/components/RouteSelector.tsx new file mode 100644 index 000000000..4a4d9ebbe --- /dev/null +++ b/web-app/src/app/components/RouteSelector.tsx @@ -0,0 +1,100 @@ +import React, { useState, useMemo } from 'react'; +import { + Drawer, + TextField, + List, + ListItem, + Checkbox, + ListItemText, + ListItemIcon, + Typography, + Box, +} from '@mui/material'; + +interface RouteSelectorProps { + routes: Array; // Replace 'any' with a more specific type if available + onSelectionChange?: (selectedRoutes: string[]) => void; + open?: boolean; +} + +export default function RouteSelector({ + routes, + onSelectionChange, + open = true, +}: RouteSelectorProps) { + const [search, setSearch] = useState(''); + const [selectedRoutes, setSelectedRoutes] = useState([]); + + const filteredRoutes = useMemo(() => { + const searchLower = search.toLowerCase(); + return routes.filter( + (route) => + route.routeName.toLowerCase().includes(searchLower) || + route.routeId.includes(searchLower), + ); + }, [search, routes]); + + const handleToggle = (routeId: string) => { + setSelectedRoutes((prev) => { + const newSelection = prev.includes(routeId) + ? prev.filter((id) => id !== routeId) + : [...prev, routeId]; + + onSelectionChange?.(newSelection); + return newSelection; + }); + }; + + return ( + + + {' '} + Select Routes{' '} + + setSearch(e.target.value)} + sx={{ mb: 2 }} + /> + + {filteredRoutes.map((route) => ( + handleToggle(route.routeId)} + > + + + + + + {route.routeName} + + } + /> + + ))} + + + ); +} diff --git a/web-app/src/app/components/sample-route-output.json b/web-app/src/app/components/sample-route-output.json new file mode 100644 index 000000000..cbe3d6de9 --- /dev/null +++ b/web-app/src/app/components/sample-route-output.json @@ -0,0 +1,3410 @@ +[ + { + "routeId": "1", + "routeName": "Ligne 1 - Verte", + "color": "#00B300", + "textColor": "#FFFFFF", + "routeType": "1", + "startDate": "20241028", + "endDate": "20250103", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "2", + "routeName": "Ligne 2 - Orange", + "color": "#D95700", + "textColor": "#FFFFFF", + "routeType": "1", + "startDate": "20241028", + "endDate": "20250103", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "4", + "routeName": "Ligne 4 - Jaune", + "color": "#FFD900", + "textColor": "#000000", + "routeType": "1", + "startDate": "20241028", + "endDate": "20250103", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "5", + "routeName": "Ligne 5 - Bleue", + "color": "#0095E6", + "textColor": "#FFFFFF", + "routeType": "1", + "startDate": "20241028", + "endDate": "20250103", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "93", + "routeName": "Jean-Talon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "16", + "routeName": "Graham", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "160", + "routeName": "Barclay", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "100", + "routeName": "Cr\u00e9mazie", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "80", + "routeName": "Avenue du Parc", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "380", + "routeName": "Henri-Bourassa", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "105", + "routeName": "Sherbrooke", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "171", + "routeName": "Henri-Bourassa", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "371", + "routeName": "D\u00e9carie", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "103", + "routeName": "Monkland", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "162", + "routeName": "Westminster", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "369", + "routeName": "C\u00f4te-des-Neiges", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "166", + "routeName": "Queen-Mary", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "119", + "routeName": "Rockland", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "124", + "routeName": "Victoria", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "128", + "routeName": "Saint-Laurent", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "363", + "routeName": "Boulevard Saint-Laurent", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "165", + "routeName": "C\u00f4te-des-Neiges", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "470", + "routeName": "Express Pierrefonds", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "711", + "routeName": "Parc-du-Mont-Royal / Oratoire", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "747", + "routeName": "YUL A\u00e9roport / Centre-Ville", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "460", + "routeName": "Express M\u00e9tropolitaine", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "164", + "routeName": "Dudemaine", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "51", + "routeName": "\u00c9douard-Montpetit", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "92", + "routeName": "Jean-Talon Ouest", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "54", + "routeName": "Charland / Chabanel", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "465", + "routeName": "Express C\u00f4te-des-Neiges", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "480", + "routeName": "Express du Parc", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "115", + "routeName": "Par\u00e9", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "95", + "routeName": "B\u00e9langer", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "47", + "routeName": "Masson", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "25", + "routeName": "Angus", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "97", + "routeName": "Avenue-du-Mont-Royal", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "192", + "routeName": "Robert", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "213", + "routeName": "Parc-Industriel-Saint-Laurent", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "170", + "routeName": "Keller", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "175", + "routeName": "Griffith / Saint-Fran\u00e7ois", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "135", + "routeName": "De l'Esplanade", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "202", + "routeName": "Dawson", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "129", + "routeName": "C\u00f4te-Sainte-Catherine", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "24", + "routeName": "Sherbrooke", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "55", + "routeName": "Boulevard Saint-Laurent", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "18", + "routeName": "Beaubien", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "409", + "routeName": "Express Des Sources", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "485", + "routeName": "Express Antoine-Faucon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "26", + "routeName": "Mercier-Est", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "186", + "routeName": "Sherbrooke-Est", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "44", + "routeName": "Armand-Bombardier", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "33", + "routeName": "Langelier", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "32", + "routeName": "Lacordaire", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "28", + "routeName": "Honor\u00e9-Beaugrand", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "187", + "routeName": "Ren\u00e9-L\u00e9vesque", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "189", + "routeName": "Notre-Dame", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "86", + "routeName": "Pointe-aux-Trembles", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "362", + "routeName": "Hochelaga / Notre-Dame", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "49", + "routeName": "Maurice-Duplessis", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "43", + "routeName": "Monselet", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "372", + "routeName": "Jean-Talon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "141", + "routeName": "Jean-Talon Est", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "364", + "routeName": "Sherbrooke / Joseph-Renaud", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "355", + "routeName": "Pie-IX", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "353", + "routeName": "Lacordaire / Maurice-Duplessis", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "81", + "routeName": "Saint-Jean-Baptiste", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "183", + "routeName": "Gouin Est", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "40", + "routeName": "Henri-Bourassa-Est", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "188", + "routeName": "Couture", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "487", + "routeName": "Express Bout-de-l'\u00cele", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "448", + "routeName": "Express Maurice-Duplessis", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "486", + "routeName": "Express Sherbrooke", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "449", + "routeName": "Express Rivi\u00e8re-des-Prairies", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "428", + "routeName": "Express Parcs industriels de l'Est", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "432", + "routeName": "Express Lacordaire", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "39", + "routeName": "Des Grandes-Prairies", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "48", + "routeName": "Perras", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "197", + "routeName": "Rosemont", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "469", + "routeName": "Express Henri-Bourassa", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "45", + "routeName": "Papineau", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "193", + "routeName": "Jarry", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "444", + "routeName": "Express C\u00e9gep Marie-Victorin", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "822", + "routeName": "Navette Longue-Pointe", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "179", + "routeName": "De l'Acadie", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "161", + "routeName": "Van Horne", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "361", + "routeName": "Saint-Denis", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "144", + "routeName": "Avenue des Pins", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "30", + "routeName": "Saint-Denis / Saint-Hubert", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "31", + "routeName": "Saint-Denis", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "66", + "routeName": "The Boulevard", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "370", + "routeName": "Rosemont", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "99", + "routeName": "Villeray", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "13", + "routeName": "Christophe-Colomb", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "445", + "routeName": "Express Papineau", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "46", + "routeName": "Casgrain", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "52", + "routeName": "de Li\u00e8ge", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "427", + "routeName": "Express Saint-Joseph", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "41", + "routeName": "Quartier Saint-Michel / Ahuntsic", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "108", + "routeName": "Bannantyne", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "12", + "routeName": "\u00cele-des-Soeurs", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "57", + "routeName": "Charlevoix", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "90", + "routeName": "Saint-Jacques", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "71", + "routeName": "Pointe-Saint-Charles", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "113", + "routeName": "Lapierre", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "496", + "routeName": "Express Victoria", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "35", + "routeName": "Griffintown", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "112", + "routeName": "Airlie", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "114", + "routeName": "Angrignon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "106", + "routeName": "Newman", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "110", + "routeName": "Centrale", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "350", + "routeName": "Verdun / LaSalle", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "198", + "routeName": "Broadway", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "36", + "routeName": "Monk", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "37", + "routeName": "Jolicoeur", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "195", + "routeName": "Dorval / Angrignon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "38", + "routeName": "De l'\u00c9glise", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "356", + "routeName": "Lachine / YUL A\u00e9roport / Des Sources", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "102", + "routeName": "Somerled", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "107", + "routeName": "Verdun", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "61", + "routeName": "Wellington", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "360", + "routeName": "Avenue des Pins", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "104", + "routeName": "Cavendish", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "63", + "routeName": "Girouard", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "211", + "routeName": "Bord-du-Lac", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "354", + "routeName": "Sainte-Anne-de-Bellevue / Centre-ville", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "123", + "routeName": "Dollard / Shevchenko", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "138", + "routeName": "Notre-Dame-de-Gr\u00e2ce", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "190", + "routeName": "Norman", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "425", + "routeName": "Express Anse-\u00e0-l'Orme", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "405", + "routeName": "Express Bord-du-Lac", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "411", + "routeName": "Express Lionel-Groulx", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "101", + "routeName": "Saint-Patrick", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "491", + "routeName": "Express Provost", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "420", + "routeName": "Express Notre-Dame-de-Gr\u00e2ce", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "11", + "routeName": "Parc-du-Mont-Royal / Ridgewood", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "27", + "routeName": "Boulevard Saint-Joseph", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "368", + "routeName": "Avenue-du-Mont-Royal", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "14", + "routeName": "Atateken", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "359", + "routeName": "Papineau", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "410", + "routeName": "Express Notre-Dame", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "430", + "routeName": "Express Pointe-aux-Trembles", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "19", + "routeName": "Chabanel / March\u00e9 Central", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "139", + "routeName": "Pie-IX", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "365", + "routeName": "Avenue du Parc", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "140", + "routeName": "Fleury", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "146", + "routeName": "Christophe-Colomb / Meilleur", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "180", + "routeName": "De Salaberry", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "67", + "routeName": "Saint-Michel", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "69", + "routeName": "Gouin", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "121", + "routeName": "Sauv\u00e9 / C\u00f4te-Vertu", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "439", + "routeName": "Express Pie-IX", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "440", + "routeName": "Express Charleroi", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "131", + "routeName": "De l'Assomption", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "136", + "routeName": "Viau", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "56", + "routeName": "Saint-Hubert", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "177", + "routeName": "Thimens", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "174", + "routeName": "C\u00f4te-Vertu-Ouest", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "64", + "routeName": "Grenet", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "467", + "routeName": "Express Saint-Michel", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "10", + "routeName": "De Lorimier", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "150", + "routeName": "Ren\u00e9-L\u00e9vesque", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "358", + "routeName": "Sainte-Catherine", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "22", + "routeName": "Notre-Dame", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "29", + "routeName": "Rachel", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "125", + "routeName": "Ontario", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "168", + "routeName": "Cit\u00e9-du-Havre", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "357", + "routeName": "Saint-Michel", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "34", + "routeName": "Sainte-Catherine", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "50", + "routeName": "Vieux-Montr\u00e9al / Vieux-Port", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "74", + "routeName": "Bridge", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "85", + "routeName": "Hochelaga", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "94", + "routeName": "D'Iberville", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "185", + "routeName": "Sherbrooke", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "176", + "routeName": "Berlioz", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "777", + "routeName": "Jean-Drapeau / Casino / Bonaventure", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "172", + "routeName": "Du Golf", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "811", + "routeName": "Navette Services sant\u00e9", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "872", + "routeName": "\u00cele-des-Soeurs", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "17", + "routeName": "D\u00e9carie", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "207", + "routeName": "Jacques-Bizard", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "215", + "routeName": "Henri-Bourassa", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "205", + "routeName": "Gouin", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "204", + "routeName": "Cardinal", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "68", + "routeName": "Pierrefonds", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "70", + "routeName": "Bois-Franc", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "201", + "routeName": "Saint-Charles / Saint-Jean", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "208", + "routeName": "Brunswick", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "206", + "routeName": "Roger-Pilon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "382", + "routeName": "Pierrefonds / Saint-Charles", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "209", + "routeName": "Des Sources", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "378", + "routeName": "Sauv\u00e9 / YUL A\u00e9roport", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "217", + "routeName": "Anse-\u00e0-l'Orme", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "200", + "routeName": "Sainte-Anne-de-Bellevue", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "203", + "routeName": "Carson", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "376", + "routeName": "Pierrefonds / Centre-ville", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "117", + "routeName": "O'Brien", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "968", + "routeName": "Trainbus Roxboro / C\u00f4te-Vertu", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "216", + "routeName": "Transcanadienne", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "225", + "routeName": "Hymus", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "72", + "routeName": "Alfred-Nobel", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "219", + "routeName": "Chemin Sainte-Marie", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "401", + "routeName": "Express Saint-Charles", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "218", + "routeName": "Antoine-Faucon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "73", + "routeName": "Dalton", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "196", + "routeName": "Parc-Industriel-Lachine", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "468", + "routeName": "Express Pierrefonds / Gouin", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "126", + "routeName": "Thimens / Grenet", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "220", + "routeName": "Kieran", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "475", + "routeName": "Express Dollard-des-Ormeaux", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "407", + "routeName": "Express \u00cele-Bizard", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "419", + "routeName": "Express John Abbott", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "212", + "routeName": "Sainte-Anne", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + } +] \ No newline at end of file diff --git a/web-app/src/app/router/Router.tsx b/web-app/src/app/router/Router.tsx index 4a021ecf5..48ad4caeb 100644 --- a/web-app/src/app/router/Router.tsx +++ b/web-app/src/app/router/Router.tsx @@ -34,6 +34,7 @@ import GBFSFeedAnalytics from '../screens/Analytics/GBFSFeedAnalytics'; import GBFSNoticeAnalytics from '../screens/Analytics/GBFSNoticeAnalytics'; import GBFSVersionAnalytics from '../screens/Analytics/GBFSVersionAnalytics'; import ContactUs from '../screens/ContactUs'; +import FullMapView from '../screens/Feed/components/FullMapView'; export const AppRouter: React.FC = () => { const navigateTo = useNavigate(); @@ -101,6 +102,8 @@ export const AppRouter: React.FC = () => { /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/web-app/src/app/screens/Feed/Feed.styles.ts b/web-app/src/app/screens/Feed/Feed.styles.ts index 1c842a352..6c70f6be3 100644 --- a/web-app/src/app/screens/Feed/Feed.styles.ts +++ b/web-app/src/app/screens/Feed/Feed.styles.ts @@ -37,6 +37,7 @@ export const featureChipsStyle: SxProps = (theme) => ({ }); export const mapBoxPositionStyle = { + position: 'relative', width: 'calc(100% + 32px)', flexGrow: 1, mb: '-16px', @@ -68,11 +69,14 @@ export const StyledTitleContainer = styled(Box)(({ theme }) => ({ alignItems: 'center', })); -export const ResponsiveListItem = styled('li')(({ theme }) => ({ +export const StyledListItem = styled('li')(({ theme }) => ({ width: '100%', margin: '5px 0', fontWeight: 'normal', fontSize: '16px', +})) + +export const ResponsiveListItem = styled(StyledListItem)(({ theme }) => ({ [theme.breakpoints.up('lg')]: { width: 'calc(50% - 15px)', }, diff --git a/web-app/src/app/screens/Feed/components/FeedSummary.tsx b/web-app/src/app/screens/Feed/components/FeedSummary.tsx index dc8fece96..1737b07f0 100644 --- a/web-app/src/app/screens/Feed/components/FeedSummary.tsx +++ b/web-app/src/app/screens/Feed/components/FeedSummary.tsx @@ -40,6 +40,7 @@ import { ResponsiveListItem, boxElementStyleProducerURL, featureChipsStyle, + StyledListItem, } from '../Feed.styles'; import FeedAuthenticationSummaryInfo from './FeedAuthenticationSummaryInfo'; @@ -104,9 +105,15 @@ export default function FeedSummary({ listStyle: providersToDisplay.length <= 1 ? 'none' : undefined, }} > - {providersToDisplay.map((provider) => ( - {provider} - ))} + {providersToDisplay.map((provider) => + providersToDisplay.length <= 1 ? ( + {provider} + ) : ( + + {provider} + + ), + )} {!showAllProviders && sortedProviders.length > 4 && ( @@ -132,6 +139,21 @@ export default function FeedSummary({ )} + {feed?.data_type === 'gtfs' && ( + + + {/* TODO: get back to this */} + + + Number of Routes + + + + 500 + + + + )} {latestDataset?.agency_timezone != undefined && ( diff --git a/web-app/src/app/screens/Feed/components/FullMapView.tsx b/web-app/src/app/screens/Feed/components/FullMapView.tsx new file mode 100644 index 000000000..66ce6d2f9 --- /dev/null +++ b/web-app/src/app/screens/Feed/components/FullMapView.tsx @@ -0,0 +1,48 @@ +import { Box, Fab } from '@mui/material'; +import RouteSelector from '../../../components/RouteSelector'; +import sampleRoutes from './sample-route-output.json'; +import { useState } from 'react'; +import { GtfsVisualizationMap } from '../../../components/GtfsVisualizationMap'; +import CloseIcon from '@mui/icons-material/Close'; + +export interface FullMapViewProps {} + +export default function FullMapView({}: FullMapViewProps): React.ReactElement { + const [filteredRoutes, setFilteredRoutes] = useState([]); + const bb = [ + [45.402668, -73.956204], + [45.402668, -73.480581], + [45.701116, -73.480581], + [45.701116, -73.956204], + ]; + return ( + + window.history.back()} + > + + + setFilteredRoutes(val)} + > + + + + + ); +} diff --git a/web-app/src/app/screens/Feed/components/sample-route-output.json b/web-app/src/app/screens/Feed/components/sample-route-output.json new file mode 100644 index 000000000..cbe3d6de9 --- /dev/null +++ b/web-app/src/app/screens/Feed/components/sample-route-output.json @@ -0,0 +1,3410 @@ +[ + { + "routeId": "1", + "routeName": "Ligne 1 - Verte", + "color": "#00B300", + "textColor": "#FFFFFF", + "routeType": "1", + "startDate": "20241028", + "endDate": "20250103", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "2", + "routeName": "Ligne 2 - Orange", + "color": "#D95700", + "textColor": "#FFFFFF", + "routeType": "1", + "startDate": "20241028", + "endDate": "20250103", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "4", + "routeName": "Ligne 4 - Jaune", + "color": "#FFD900", + "textColor": "#000000", + "routeType": "1", + "startDate": "20241028", + "endDate": "20250103", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "5", + "routeName": "Ligne 5 - Bleue", + "color": "#0095E6", + "textColor": "#FFFFFF", + "routeType": "1", + "startDate": "20241028", + "endDate": "20250103", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "93", + "routeName": "Jean-Talon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "16", + "routeName": "Graham", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "160", + "routeName": "Barclay", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "100", + "routeName": "Cr\u00e9mazie", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "80", + "routeName": "Avenue du Parc", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "380", + "routeName": "Henri-Bourassa", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "105", + "routeName": "Sherbrooke", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "171", + "routeName": "Henri-Bourassa", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "371", + "routeName": "D\u00e9carie", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "103", + "routeName": "Monkland", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "162", + "routeName": "Westminster", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "369", + "routeName": "C\u00f4te-des-Neiges", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "166", + "routeName": "Queen-Mary", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "119", + "routeName": "Rockland", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "124", + "routeName": "Victoria", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "128", + "routeName": "Saint-Laurent", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "363", + "routeName": "Boulevard Saint-Laurent", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "165", + "routeName": "C\u00f4te-des-Neiges", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "470", + "routeName": "Express Pierrefonds", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "711", + "routeName": "Parc-du-Mont-Royal / Oratoire", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "747", + "routeName": "YUL A\u00e9roport / Centre-Ville", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "460", + "routeName": "Express M\u00e9tropolitaine", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "164", + "routeName": "Dudemaine", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "51", + "routeName": "\u00c9douard-Montpetit", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "92", + "routeName": "Jean-Talon Ouest", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "54", + "routeName": "Charland / Chabanel", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "465", + "routeName": "Express C\u00f4te-des-Neiges", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "480", + "routeName": "Express du Parc", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "115", + "routeName": "Par\u00e9", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "95", + "routeName": "B\u00e9langer", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "47", + "routeName": "Masson", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "25", + "routeName": "Angus", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "97", + "routeName": "Avenue-du-Mont-Royal", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "192", + "routeName": "Robert", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "213", + "routeName": "Parc-Industriel-Saint-Laurent", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "170", + "routeName": "Keller", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "175", + "routeName": "Griffith / Saint-Fran\u00e7ois", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "135", + "routeName": "De l'Esplanade", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "202", + "routeName": "Dawson", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "129", + "routeName": "C\u00f4te-Sainte-Catherine", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "24", + "routeName": "Sherbrooke", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "55", + "routeName": "Boulevard Saint-Laurent", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "18", + "routeName": "Beaubien", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "409", + "routeName": "Express Des Sources", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "485", + "routeName": "Express Antoine-Faucon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "26", + "routeName": "Mercier-Est", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "186", + "routeName": "Sherbrooke-Est", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "44", + "routeName": "Armand-Bombardier", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "33", + "routeName": "Langelier", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "32", + "routeName": "Lacordaire", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "28", + "routeName": "Honor\u00e9-Beaugrand", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "187", + "routeName": "Ren\u00e9-L\u00e9vesque", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "189", + "routeName": "Notre-Dame", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "86", + "routeName": "Pointe-aux-Trembles", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "362", + "routeName": "Hochelaga / Notre-Dame", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "49", + "routeName": "Maurice-Duplessis", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "43", + "routeName": "Monselet", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "372", + "routeName": "Jean-Talon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "141", + "routeName": "Jean-Talon Est", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "364", + "routeName": "Sherbrooke / Joseph-Renaud", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "355", + "routeName": "Pie-IX", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "353", + "routeName": "Lacordaire / Maurice-Duplessis", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "81", + "routeName": "Saint-Jean-Baptiste", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "183", + "routeName": "Gouin Est", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "40", + "routeName": "Henri-Bourassa-Est", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "188", + "routeName": "Couture", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "487", + "routeName": "Express Bout-de-l'\u00cele", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "448", + "routeName": "Express Maurice-Duplessis", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "486", + "routeName": "Express Sherbrooke", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "449", + "routeName": "Express Rivi\u00e8re-des-Prairies", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "428", + "routeName": "Express Parcs industriels de l'Est", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "432", + "routeName": "Express Lacordaire", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "39", + "routeName": "Des Grandes-Prairies", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "48", + "routeName": "Perras", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "197", + "routeName": "Rosemont", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "469", + "routeName": "Express Henri-Bourassa", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "45", + "routeName": "Papineau", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "193", + "routeName": "Jarry", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "444", + "routeName": "Express C\u00e9gep Marie-Victorin", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "822", + "routeName": "Navette Longue-Pointe", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "179", + "routeName": "De l'Acadie", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "161", + "routeName": "Van Horne", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "361", + "routeName": "Saint-Denis", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "144", + "routeName": "Avenue des Pins", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "30", + "routeName": "Saint-Denis / Saint-Hubert", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "31", + "routeName": "Saint-Denis", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "66", + "routeName": "The Boulevard", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "370", + "routeName": "Rosemont", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "99", + "routeName": "Villeray", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "13", + "routeName": "Christophe-Colomb", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "445", + "routeName": "Express Papineau", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "46", + "routeName": "Casgrain", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "52", + "routeName": "de Li\u00e8ge", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "427", + "routeName": "Express Saint-Joseph", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "41", + "routeName": "Quartier Saint-Michel / Ahuntsic", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "108", + "routeName": "Bannantyne", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "12", + "routeName": "\u00cele-des-Soeurs", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "57", + "routeName": "Charlevoix", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "90", + "routeName": "Saint-Jacques", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "71", + "routeName": "Pointe-Saint-Charles", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "113", + "routeName": "Lapierre", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "496", + "routeName": "Express Victoria", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "35", + "routeName": "Griffintown", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "112", + "routeName": "Airlie", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "114", + "routeName": "Angrignon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "106", + "routeName": "Newman", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "110", + "routeName": "Centrale", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "350", + "routeName": "Verdun / LaSalle", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "198", + "routeName": "Broadway", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "36", + "routeName": "Monk", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "37", + "routeName": "Jolicoeur", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "195", + "routeName": "Dorval / Angrignon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "38", + "routeName": "De l'\u00c9glise", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "356", + "routeName": "Lachine / YUL A\u00e9roport / Des Sources", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "102", + "routeName": "Somerled", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "107", + "routeName": "Verdun", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "61", + "routeName": "Wellington", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "360", + "routeName": "Avenue des Pins", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "104", + "routeName": "Cavendish", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "63", + "routeName": "Girouard", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "211", + "routeName": "Bord-du-Lac", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "354", + "routeName": "Sainte-Anne-de-Bellevue / Centre-ville", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "123", + "routeName": "Dollard / Shevchenko", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "138", + "routeName": "Notre-Dame-de-Gr\u00e2ce", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "190", + "routeName": "Norman", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "425", + "routeName": "Express Anse-\u00e0-l'Orme", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "405", + "routeName": "Express Bord-du-Lac", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "411", + "routeName": "Express Lionel-Groulx", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "101", + "routeName": "Saint-Patrick", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "491", + "routeName": "Express Provost", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "420", + "routeName": "Express Notre-Dame-de-Gr\u00e2ce", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "11", + "routeName": "Parc-du-Mont-Royal / Ridgewood", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "27", + "routeName": "Boulevard Saint-Joseph", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "368", + "routeName": "Avenue-du-Mont-Royal", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "14", + "routeName": "Atateken", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "359", + "routeName": "Papineau", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "410", + "routeName": "Express Notre-Dame", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "430", + "routeName": "Express Pointe-aux-Trembles", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "19", + "routeName": "Chabanel / March\u00e9 Central", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "139", + "routeName": "Pie-IX", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "365", + "routeName": "Avenue du Parc", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "140", + "routeName": "Fleury", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "146", + "routeName": "Christophe-Colomb / Meilleur", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "180", + "routeName": "De Salaberry", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "67", + "routeName": "Saint-Michel", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "69", + "routeName": "Gouin", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "121", + "routeName": "Sauv\u00e9 / C\u00f4te-Vertu", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "439", + "routeName": "Express Pie-IX", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "440", + "routeName": "Express Charleroi", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "131", + "routeName": "De l'Assomption", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "136", + "routeName": "Viau", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "56", + "routeName": "Saint-Hubert", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "177", + "routeName": "Thimens", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "174", + "routeName": "C\u00f4te-Vertu-Ouest", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "64", + "routeName": "Grenet", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "467", + "routeName": "Express Saint-Michel", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "10", + "routeName": "De Lorimier", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "150", + "routeName": "Ren\u00e9-L\u00e9vesque", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "358", + "routeName": "Sainte-Catherine", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "22", + "routeName": "Notre-Dame", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "29", + "routeName": "Rachel", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "125", + "routeName": "Ontario", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "168", + "routeName": "Cit\u00e9-du-Havre", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "357", + "routeName": "Saint-Michel", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "34", + "routeName": "Sainte-Catherine", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "50", + "routeName": "Vieux-Montr\u00e9al / Vieux-Port", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "74", + "routeName": "Bridge", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "85", + "routeName": "Hochelaga", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "94", + "routeName": "D'Iberville", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "185", + "routeName": "Sherbrooke", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "176", + "routeName": "Berlioz", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "777", + "routeName": "Jean-Drapeau / Casino / Bonaventure", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "172", + "routeName": "Du Golf", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "811", + "routeName": "Navette Services sant\u00e9", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "872", + "routeName": "\u00cele-des-Soeurs", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "17", + "routeName": "D\u00e9carie", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "207", + "routeName": "Jacques-Bizard", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "215", + "routeName": "Henri-Bourassa", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "205", + "routeName": "Gouin", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "204", + "routeName": "Cardinal", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "68", + "routeName": "Pierrefonds", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "70", + "routeName": "Bois-Franc", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "201", + "routeName": "Saint-Charles / Saint-Jean", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "208", + "routeName": "Brunswick", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "206", + "routeName": "Roger-Pilon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "382", + "routeName": "Pierrefonds / Saint-Charles", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "209", + "routeName": "Des Sources", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "378", + "routeName": "Sauv\u00e9 / YUL A\u00e9roport", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "217", + "routeName": "Anse-\u00e0-l'Orme", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "200", + "routeName": "Sainte-Anne-de-Bellevue", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "203", + "routeName": "Carson", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "376", + "routeName": "Pierrefonds / Centre-ville", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "117", + "routeName": "O'Brien", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "968", + "routeName": "Trainbus Roxboro / C\u00f4te-Vertu", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + }, + { + "routeId": "216", + "routeName": "Transcanadienne", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "225", + "routeName": "Hymus", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "72", + "routeName": "Alfred-Nobel", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "219", + "routeName": "Chemin Sainte-Marie", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "401", + "routeName": "Express Saint-Charles", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "218", + "routeName": "Antoine-Faucon", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "73", + "routeName": "Dalton", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "196", + "routeName": "Parc-Industriel-Lachine", + "color": "#781B7D", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "468", + "routeName": "Express Pierrefonds / Gouin", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "126", + "routeName": "Thimens / Grenet", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "220", + "routeName": "Kieran", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "475", + "routeName": "Express Dollard-des-Ormeaux", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "407", + "routeName": "Express \u00cele-Bizard", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "419", + "routeName": "Express John Abbott", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241028", + "endDate": "20241220", + "monday": true, + "tuesday": true, + "wednesday": true, + "thursday": true, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "routeId": "212", + "routeName": "Sainte-Anne", + "color": "#009EE0", + "textColor": "#FFFFFF", + "routeType": "3", + "startDate": "20241102", + "endDate": "20241221", + "monday": false, + "tuesday": false, + "wednesday": false, + "thursday": false, + "friday": false, + "saturday": true, + "sunday": false + } +] \ No newline at end of file diff --git a/web-app/yarn.lock b/web-app/yarn.lock index ef73f9a02..6011cde5b 100644 --- a/web-app/yarn.lock +++ b/web-app/yarn.lock @@ -2416,6 +2416,71 @@ resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@mapbox/geojson-rewind@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz#591a5d71a9cd1da1a0bf3420b3bea31b0fc7946a" + integrity sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA== + dependencies: + get-stream "^6.0.1" + minimist "^1.2.6" + +"@mapbox/jsonlint-lines-primitives@^2.0.2", "@mapbox/jsonlint-lines-primitives@~2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" + integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ== + +"@mapbox/point-geometry@0.1.0", "@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" + integrity sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ== + +"@mapbox/tiny-sdf@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz#9a1d33e5018093e88f6a4df2343e886056287282" + integrity sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA== + +"@mapbox/unitbezier@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz#d32deb66c7177e9e9dfc3bbd697083e2e657ff01" + integrity sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw== + +"@mapbox/vector-tile@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz#d3a74c90402d06e89ec66de49ec817ff53409666" + integrity sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw== + dependencies: + "@mapbox/point-geometry" "~0.1.0" + +"@mapbox/whoots-js@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz#497c67a1cef50d1a2459ba60f315e448d2ad87fe" + integrity sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q== + +"@maplibre/maplibre-gl-style-spec@^19.2.1": + version "19.3.3" + resolved "https://registry.yarnpkg.com/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz#a106248bd2e25e77c963a362aeaf630e00f924e9" + integrity sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw== + dependencies: + "@mapbox/jsonlint-lines-primitives" "~2.0.2" + "@mapbox/unitbezier" "^0.0.1" + json-stringify-pretty-compact "^3.0.0" + minimist "^1.2.8" + rw "^1.3.3" + sort-object "^3.0.3" + +"@maplibre/maplibre-gl-style-spec@^20.3.1": + version "20.4.0" + resolved "https://registry.yarnpkg.com/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz#408339e051fb51e022b40af2235e0beb037937ea" + integrity sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw== + dependencies: + "@mapbox/jsonlint-lines-primitives" "~2.0.2" + "@mapbox/unitbezier" "^0.0.1" + json-stringify-pretty-compact "^4.0.0" + minimist "^1.2.8" + quickselect "^2.0.0" + rw "^1.3.3" + tinyqueue "^3.0.0" + "@mui/base@^5.0.0-beta.20": version "5.0.0-beta.30" resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.30.tgz" @@ -3326,11 +3391,23 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/geojson-vt@3.2.5": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@types/geojson-vt/-/geojson-vt-3.2.5.tgz#b6c356874991d9ab4207533476dfbcdb21e38408" + integrity sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g== + dependencies: + "@types/geojson" "*" + "@types/geojson@*": version "7946.0.14" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== +"@types/geojson@^7946.0.14": + version "7946.0.16" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== + "@types/glob@*": version "8.1.0" resolved "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz" @@ -3445,6 +3522,27 @@ resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== +"@types/mapbox-gl@*", "@types/mapbox-gl@>=1.0.0": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz#b6a1b81b802e62fb7e1d1d13a97ccece63b232a3" + integrity sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg== + dependencies: + "@types/geojson" "*" + +"@types/mapbox__point-geometry@*", "@types/mapbox__point-geometry@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz#0ef017b75eedce02ff6243b4189210e2e6d5e56d" + integrity sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA== + +"@types/mapbox__vector-tile@^1.3.4": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz#ad757441ef1d34628d9e098afd9c91423c1f8734" + integrity sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg== + dependencies: + "@types/geojson" "*" + "@types/mapbox__point-geometry" "*" + "@types/pbf" "*" + "@types/markdown-it@^12.2.3": version "12.2.3" resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz" @@ -3525,6 +3623,11 @@ resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== +"@types/pbf@*", "@types/pbf@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.5.tgz#a9495a58d8c75be4ffe9a0bd749a307715c07404" + integrity sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA== + "@types/prettier@^2.1.5": version "2.7.3" resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz" @@ -3576,6 +3679,16 @@ dependencies: "@types/react" "*" +"@types/react-map-gl@^6.1.7": + version "6.1.7" + resolved "https://registry.yarnpkg.com/@types/react-map-gl/-/react-map-gl-6.1.7.tgz#c3e81ccd21c31cee8fe1576eab894cf6d4be9f0a" + integrity sha512-szkfkWd3FbySDkxyn0MDj9yzD8XYk+RIi4od6sGb3lVHNBGcW20G2v2vcq2N5k18UYAdqAoKGSYuHkGV4JOCrA== + dependencies: + "@types/geojson" "*" + "@types/mapbox-gl" "*" + "@types/react" "*" + "@types/viewport-mercator-project" "*" + "@types/react-redux@^7.1.27": version "7.1.31" resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.31.tgz" @@ -3709,6 +3822,13 @@ resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/supercluster@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/supercluster/-/supercluster-7.1.3.tgz#1a1bc2401b09174d9c9e44124931ec7874a72b27" + integrity sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA== + dependencies: + "@types/geojson" "*" + "@types/testing-library__jest-dom@^5.9.1": version "5.14.9" resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz" @@ -3731,6 +3851,13 @@ resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== +"@types/viewport-mercator-project@*": + version "6.1.6" + resolved "https://registry.yarnpkg.com/@types/viewport-mercator-project/-/viewport-mercator-project-6.1.6.tgz#74874286099e9506943c3f868c9cbeaf2a6d1559" + integrity sha512-uWrbqhRXFeiT6CAvRjf0BkQKRkKED+ofrPhglKpUktQML3463dEPiA4iwe7cZQs6m49Zo/g03rL7ChMLiE5Z8w== + dependencies: + gl-matrix "^3.2.0" + "@types/ws@^8.5.5": version "8.5.10" resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz" @@ -4372,6 +4499,11 @@ aria-query@^5.0.0, aria-query@^5.3.0: dependencies: dequal "^2.0.3" +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== + array-buffer-byte-length@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz" @@ -4504,6 +4636,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw== + ast-types-flow@^0.0.8: version "0.0.8" resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz" @@ -5018,6 +5155,21 @@ bytes@3.1.2: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +bytewise-core@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/bytewise-core/-/bytewise-core-1.2.3.tgz#3fb410c7e91558eb1ab22a82834577aa6bd61d42" + integrity sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA== + dependencies: + typewise-core "^1.2" + +bytewise@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/bytewise/-/bytewise-1.1.0.tgz#1d13cbff717ae7158094aa881b35d081b387253e" + integrity sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ== + dependencies: + bytewise-core "^1.2.2" + typewise "^1.0.3" + cacache@^18.0.0: version "18.0.0" resolved "https://registry.npmjs.org/cacache/-/cacache-18.0.0.tgz" @@ -6438,6 +6590,11 @@ duplexify@^4.0.0: readable-stream "^3.1.1" stream-shift "^1.0.0" +earcut@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/earcut/-/earcut-3.0.1.tgz#f60b3f671c5657cca9d3e131c5527c5dde00ef38" + integrity sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw== + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" @@ -7272,6 +7429,21 @@ express@^4.16.4, express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q== + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" @@ -7388,6 +7560,11 @@ fecha@^4.2.0: resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz" integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== +fflate@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + figures@^3.0.0, figures@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" @@ -7838,6 +8015,11 @@ gensync@^1.0.0-beta.2: resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +geojson-vt@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-4.0.2.tgz#1162f6c7d61a0ba305b1030621e6e111f847828a" + integrity sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" @@ -7900,6 +8082,11 @@ get-uri@^6.0.1: debug "^4.3.4" fs-extra "^8.1.0" +get-value@^2.0.2, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== + getos@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz" @@ -7914,6 +8101,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +gl-matrix@^3.2.0, gl-matrix@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.4.3.tgz#fc1191e8320009fd4d20e9339595c6041ddc22c9" + integrity sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" @@ -8016,6 +8208,15 @@ global-prefix@^3.0.0: kind-of "^6.0.2" which "^1.3.1" +global-prefix@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-4.0.0.tgz#e9cc79aab9be1d03287e156a3f912dd0895463ed" + integrity sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA== + dependencies: + ini "^4.1.3" + kind-of "^6.0.3" + which "^4.0.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" @@ -8507,9 +8708,9 @@ identity-obj-proxy@^3.0.0: dependencies: harmony-reflect "^1.4.6" -ieee754@^1.1.13: +ieee754@^1.1.12, ieee754@^1.1.13: version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^5.2.0, ignore@^5.2.4: @@ -8581,6 +8782,11 @@ ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +ini@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795" + integrity sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg== + inquirer@^8.2.0: version "8.2.6" resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz" @@ -8759,6 +8965,18 @@ is-docker@^3.0.0: resolved "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz" integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" @@ -8872,6 +9090,13 @@ is-plain-obj@^3.0.0: resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" @@ -9029,6 +9254,11 @@ isexe@^3.1.1: resolved "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz" integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + isomorphic-fetch@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz" @@ -9857,6 +10087,16 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stringify-pretty-compact@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz#f71ef9d82ef16483a407869556588e91b681d9ab" + integrity sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA== + +json-stringify-pretty-compact@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz#cf4844770bddee3cb89a6170fe4b00eee5dbf1d4" + integrity sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q== + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" @@ -9984,6 +10224,11 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" +kdbush@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-4.0.2.tgz#2f7b7246328b4657dd122b6c7f025fbc2c868e39" + integrity sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA== + keyv@^4.5.3: version "4.5.4" resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" @@ -9991,9 +10236,9 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" -kind-of@^6.0.2: +kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== klaw@^3.0.0: @@ -10435,6 +10680,38 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +maplibre-gl@^4.7.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-4.7.1.tgz#06a524438ee2aafbe8bcd91002a4e01468ea5486" + integrity sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA== + dependencies: + "@mapbox/geojson-rewind" "^0.5.2" + "@mapbox/jsonlint-lines-primitives" "^2.0.2" + "@mapbox/point-geometry" "^0.1.0" + "@mapbox/tiny-sdf" "^2.0.6" + "@mapbox/unitbezier" "^0.0.1" + "@mapbox/vector-tile" "^1.3.1" + "@mapbox/whoots-js" "^3.1.0" + "@maplibre/maplibre-gl-style-spec" "^20.3.1" + "@types/geojson" "^7946.0.14" + "@types/geojson-vt" "3.2.5" + "@types/mapbox__point-geometry" "^0.1.4" + "@types/mapbox__vector-tile" "^1.3.4" + "@types/pbf" "^3.0.5" + "@types/supercluster" "^7.1.3" + earcut "^3.0.0" + geojson-vt "^4.0.2" + gl-matrix "^3.4.3" + global-prefix "^4.0.0" + kdbush "^4.0.2" + murmurhash-js "^1.0.0" + pbf "^3.3.0" + potpack "^2.0.0" + quickselect "^3.0.0" + supercluster "^8.0.1" + tinyqueue "^3.0.0" + vt-pbf "^3.1.3" + markdown-it-anchor@^8.4.1: version "8.6.7" resolved "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz" @@ -10754,6 +11031,11 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" +murmurhash-js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" + integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw== + mute-stream@0.0.8: version "0.0.8" resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" @@ -11366,6 +11648,14 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pbf@^3.2.1, pbf@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.3.0.tgz#1790f3d99118333cc7f498de816028a346ef367f" + integrity sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q== + dependencies: + ieee754 "^1.1.12" + resolve-protobuf-schema "^2.1.0" + pend@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" @@ -11415,6 +11705,13 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" +pmtiles@^4.2.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/pmtiles/-/pmtiles-4.3.0.tgz#65066296103ee199e8a09d399377a322ab376a14" + integrity sha512-wnzQeSiYT/MyO63o7AVxwt7+uKqU0QUy2lHrivM7GvecNy0m1A4voVyGey7bujnEW5Hn+ZzLdvHPoFaqrOzbPA== + dependencies: + fflate "^0.8.2" + portfinder@^1.0.32: version "1.0.32" resolved "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz" @@ -11976,6 +12273,11 @@ postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.4: picocolors "^1.0.0" source-map-js "^1.0.2" +potpack@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/potpack/-/potpack-2.0.0.tgz#61f4dd2dc4b3d5e996e3698c0ec9426d0e169104" + integrity sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -12165,6 +12467,11 @@ protobufjs@^7.0.0, protobufjs@^7.2.4: "@types/node" ">=13.7.0" long "^5.0.0" +protocol-buffers-schema@^3.3.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" + integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" @@ -12268,6 +12575,16 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quickselect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" + integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== + +quickselect@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-3.0.0.tgz#a37fc953867d56f095a20ac71c6d27063d2de603" + integrity sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g== + raf@^3.2.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" @@ -12498,6 +12815,14 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-map-gl@^7.1.7: + version "7.1.9" + resolved "https://registry.yarnpkg.com/react-map-gl/-/react-map-gl-7.1.9.tgz#d41093ce266c342f89a2c49dec3739e980835fa7" + integrity sha512-KsCc8Gyn05wVGlHZoopaiiCr0RCAQ6LDISo5sEy1/pV/d7RlozkF946tiX7IgyijJQMRujHol5QdwUPESjh73w== + dependencies: + "@maplibre/maplibre-gl-style-spec" "^19.2.1" + "@types/mapbox-gl" ">=1.0.0" + react-redux@^8.1.3: version "8.1.3" resolved "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz" @@ -12957,6 +13282,13 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== +resolve-protobuf-schema@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758" + integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ== + dependencies: + protocol-buffers-schema "^3.3.1" + resolve-url-loader@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz" @@ -13083,6 +13415,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rw@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== + rxjs@^7.5.1, rxjs@^7.5.5, rxjs@^7.8.1: version "7.8.1" resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" @@ -13318,6 +13655,16 @@ set-function-name@^2.0.0, set-function-name@^2.0.1: functions-have-names "^1.2.3" has-property-descriptors "^1.0.0" +set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" @@ -13452,6 +13799,28 @@ socks@^2.7.1: ip "^2.0.0" smart-buffer "^4.2.0" +sort-asc@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/sort-asc/-/sort-asc-0.2.0.tgz#00a49e947bc25d510bfde2cbb8dffda9f50eb2fc" + integrity sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA== + +sort-desc@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/sort-desc/-/sort-desc-0.2.0.tgz#280c1bdafc6577887cedbad1ed2e41c037976646" + integrity sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w== + +sort-object@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sort-object/-/sort-object-3.0.3.tgz#945727165f244af9dc596ad4c7605a8dee80c269" + integrity sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ== + dependencies: + bytewise "^1.1.0" + get-value "^2.0.2" + is-extendable "^0.1.1" + sort-asc "^0.2.0" + sort-desc "^0.2.0" + union-value "^1.0.1" + source-list-map@^2.0.0, source-list-map@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz" @@ -13529,6 +13898,13 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" +split-string@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" @@ -13826,6 +14202,13 @@ sucrase@^3.32.0: pirates "^4.0.1" ts-interface-checker "^0.1.9" +supercluster@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-8.0.1.tgz#9946ba123538e9e9ab15de472531f604e7372df5" + integrity sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ== + dependencies: + kdbush "^4.0.2" + superstatic@^9.0.3: version "9.0.3" resolved "https://registry.npmjs.org/superstatic/-/superstatic-9.0.3.tgz" @@ -14122,6 +14505,11 @@ tiny-warning@^1.0.2: resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinyqueue@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-3.0.0.tgz#101ea761ccc81f979e29200929e78f1556e3661e" + integrity sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g== + titleize@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz" @@ -14413,6 +14801,18 @@ typescript@^5.2.2: resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz" integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ== +typewise-core@^1.2, typewise-core@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/typewise-core/-/typewise-core-1.2.0.tgz#97eb91805c7f55d2f941748fa50d315d991ef195" + integrity sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg== + +typewise@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typewise/-/typewise-1.0.3.tgz#1067936540af97937cc5dcf9922486e9fa284651" + integrity sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ== + dependencies: + typewise-core "^1.2.0" + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz" @@ -14483,6 +14883,16 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== +union-value@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + unique-filename@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz" @@ -14690,6 +15100,15 @@ void-elements@3.1.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== +vt-pbf@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.3.tgz#68fd150756465e2edae1cc5c048e063916dcfaac" + integrity sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA== + dependencies: + "@mapbox/point-geometry" "0.1.0" + "@mapbox/vector-tile" "^1.3.1" + pbf "^3.2.1" + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz" From d97fd077805d812930788b00a3fe708c424b5241 Mon Sep 17 00:00:00 2001 From: cka-y Date: Tue, 17 Jun 2025 16:10:11 -0400 Subject: [PATCH 02/16] pair programming pros --- .../app/components/GtfsVisualizationMap.tsx | 20 +++--- .../src/app/components/NestedCheckboxList.tsx | 1 + web-app/src/app/constants/RouteTypes.ts | 13 ++++ .../screens/Feed/components/FullMapView.tsx | 72 +++++++++++++++++-- 4 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 web-app/src/app/constants/RouteTypes.ts diff --git a/web-app/src/app/components/GtfsVisualizationMap.tsx b/web-app/src/app/components/GtfsVisualizationMap.tsx index c258ae2a8..a0710b2c9 100644 --- a/web-app/src/app/components/GtfsVisualizationMap.tsx +++ b/web-app/src/app/components/GtfsVisualizationMap.tsx @@ -6,18 +6,18 @@ import { Protocol } from 'pmtiles'; import { type LatLngExpression } from 'leaflet'; import type { FeatureCollection } from 'geojson'; import { Box, useTheme } from '@mui/material'; -import sampleRoutes from './sample-route-output.json'; import { MapElement } from './MapElement'; -import RouteSelector from './RouteSelector'; export interface GtfsVisualizationMapProps { polygon: LatLngExpression[]; filteredRoutes?: string[]; + filteredRouteTypes?: string[]; } export const GtfsVisualizationMap = ({ polygon, filteredRoutes = [], + filteredRouteTypes = [], }: GtfsVisualizationMapProps): JSX.Element => { const theme = useTheme(); const [hoverInfo, setHoverInfo] = useState([]); @@ -85,7 +85,7 @@ export const GtfsVisualizationMap = ({ }, []); const getBoundsFromCoordinates = ( - coordinates: [number, number][], + coordinates: Array<[number, number]>, ): LngLatBoundsLike => { let minLng = Number.POSITIVE_INFINITY; let minLat = Number.POSITIVE_INFINITY; @@ -103,7 +103,7 @@ export const GtfsVisualizationMap = ({ }; const bounds: LngLatBoundsLike = getBoundsFromCoordinates( - polygon as [number, number][], + polygon as Array<[number, number]>, ); // in polygon: IMPORTANT - the coordinates are in the format [lat, lng] (not [lng, lat]) @@ -135,7 +135,7 @@ export const GtfsVisualizationMap = ({ width: '100%', height: '100%', position: 'relative', - //border: '2px solid', + // border: '2px solid', borderColor: theme.palette.primary.main, borderRadius: '5px', }} @@ -144,7 +144,9 @@ export const GtfsVisualizationMap = ({ handleMouseMove(event)} + onMouseMove={(event) => { + handleMouseMove(event); + }} style={{ width: '100%', height: '100%' }} initialViewState={{ bounds }} interactiveLayerIds={['stops', 'routes', 'routes-white']} @@ -177,6 +179,7 @@ export const GtfsVisualizationMap = ({ // }, }, // Order matters: the last layer will be on top + // Layers control all the logic in the map -> lots of duplicated for the sake of effects layers: [ { id: 'simple-tiles', @@ -187,7 +190,7 @@ export const GtfsVisualizationMap = ({ }, { - id: 'routes-white', + id: 'routes-white', // white padding on the route lines source: 'routes', 'source-layer': 'routesoutput', // Name of the geojson file when converting to pmtile. route-output.geojson -> routesoutput type: 'line', @@ -214,6 +217,7 @@ export const GtfsVisualizationMap = ({ // style and color of the stops 'line-color': ['concat', '#', ['get', 'route_color']], 'line-width': [ + // line with thickness based on route type (thicker for metro, thinner for bus) 'match', ['get', 'route_type'], // Property from the vector tile '3', @@ -301,7 +305,7 @@ export const GtfsVisualizationMap = ({ filter: [ 'any', ['in', ['get', 'stop_id'], ['literal', hoverInfo]], - //['in', 2, ['get', 'route_ids']] + // ['in', 2, ['get', 'route_ids']] [ 'any', ['in', '141', ['get', 'route_ids']], diff --git a/web-app/src/app/components/NestedCheckboxList.tsx b/web-app/src/app/components/NestedCheckboxList.tsx index ebb5fcb7a..2a9ef75e1 100644 --- a/web-app/src/app/components/NestedCheckboxList.tsx +++ b/web-app/src/app/components/NestedCheckboxList.tsx @@ -28,6 +28,7 @@ export interface CheckboxStructure { seeChildren?: boolean; children?: CheckboxStructure[]; disabled?: boolean; + props?: Record; } function useDebouncedCallback(callback: () => void, delay: number): () => void { diff --git a/web-app/src/app/constants/RouteTypes.ts b/web-app/src/app/constants/RouteTypes.ts new file mode 100644 index 000000000..f7b588a16 --- /dev/null +++ b/web-app/src/app/constants/RouteTypes.ts @@ -0,0 +1,13 @@ +export const routeTypesMapping: Record = { + '0': 'Tram', + '1': 'Subway', + '2': 'Rail', + '3': 'Bus', + '4': 'Ferry', + '5': 'Cable Car', + '6': 'Gondola', + '7': 'Funicular', + '11': 'Monorail', + '12': 'High Speed Train', + '13': 'Airplane', +}; diff --git a/web-app/src/app/screens/Feed/components/FullMapView.tsx b/web-app/src/app/screens/Feed/components/FullMapView.tsx index 66ce6d2f9..837883717 100644 --- a/web-app/src/app/screens/Feed/components/FullMapView.tsx +++ b/web-app/src/app/screens/Feed/components/FullMapView.tsx @@ -1,14 +1,39 @@ import { Box, Fab } from '@mui/material'; import RouteSelector from '../../../components/RouteSelector'; import sampleRoutes from './sample-route-output.json'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { GtfsVisualizationMap } from '../../../components/GtfsVisualizationMap'; import CloseIcon from '@mui/icons-material/Close'; +import NestedCheckboxList, { + type CheckboxStructure, +} from '../../../components/NestedCheckboxList'; +import { routeTypesMapping } from '../../../constants/RouteTypes'; export interface FullMapViewProps {} export default function FullMapView({}: FullMapViewProps): React.ReactElement { const [filteredRoutes, setFilteredRoutes] = useState([]); + const [filteredRouteTypes, setFilteredRouteTypes] = useState([]); + + const getUniqueRouteTypesCheckboxData = ( + routes: Array<{ routeType: string }>, + ): CheckboxStructure[] => { + const uniqueTypes = new Set(); + routes.forEach((route) => { + if (route?.routeType !== '') { + uniqueTypes.add(route.routeType); + } + }); + return Array.from(uniqueTypes).map((type) => ({ + title: routeTypesMapping[type], + checked: true, + props: { + routeTypeId: type, + }, + type: 'checkbox', + })) as CheckboxStructure[]; + }; + const bb = [ [45.402668, -73.956204], [45.402668, -73.480581], @@ -22,24 +47,57 @@ export default function FullMapView({}: FullMapViewProps): React.ReactElement { height: '100%', position: 'relative', display: 'flex', - flexDirection: 'column', }} > window.history.back()} + onClick={() => { + window.history.back(); + }} > - setFilteredRoutes(val)} - > + + { + console.log(checkboxData); + }} + /> + {/* Route type selector */} + + { + setFilteredRouteTypes( + checkboxData + .map((item) => { + return item.checked ? item?.props?.routeTypeId ?? '' : ''; + }) + .filter((item) => item !== ''), + ); + }} + /> + + { + setFilteredRoutes(val); + }} + > + From fceb15af32c39eed6b91d7e9a08ed116179f375f Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Wed, 18 Jun 2025 17:23:10 -0400 Subject: [PATCH 03/16] styling updates --- web-app/src/app/components/ContentBox.tsx | 2 +- web-app/src/app/components/CoveredAreaMap.tsx | 163 ++++++++++-------- .../app/components/GtfsVisualizationMap.tsx | 30 ++-- web-app/src/app/components/RouteSelector.tsx | 38 ++-- .../screens/Feed/components/FeedSummary.tsx | 42 ++++- .../screens/Feed/components/FullMapView.tsx | 158 +++++++++++++---- web-app/src/app/screens/Feed/index.tsx | 20 +-- web-app/src/app/screens/Feeds/Feeds.styles.ts | 16 -- .../src/app/screens/Feeds/SearchFilters.tsx | 2 +- web-app/src/app/styles/Filters.styles.ts | 17 ++ 10 files changed, 309 insertions(+), 179 deletions(-) create mode 100644 web-app/src/app/styles/Filters.styles.ts diff --git a/web-app/src/app/components/ContentBox.tsx b/web-app/src/app/components/ContentBox.tsx index d389ecc11..9ca888ed4 100644 --- a/web-app/src/app/components/ContentBox.tsx +++ b/web-app/src/app/components/ContentBox.tsx @@ -42,7 +42,7 @@ export const ContentBox = ( mb: 1, }} > - {props.title} + {props.title.trim() !== '' && {props.title}} {props.action != null && props.action} )} diff --git a/web-app/src/app/components/CoveredAreaMap.tsx b/web-app/src/app/components/CoveredAreaMap.tsx index cf2417d85..63340d109 100644 --- a/web-app/src/app/components/CoveredAreaMap.tsx +++ b/web-app/src/app/components/CoveredAreaMap.tsx @@ -32,7 +32,7 @@ import { useSelector } from 'react-redux'; import { selectAutodiscoveryGbfsVersion } from '../store/feed-selectors'; import ModeOfTravelIcon from '@mui/icons-material/ModeOfTravel'; import { GtfsVisualizationMap } from './GtfsVisualizationMap'; -import OpenInFullIcon from '@mui/icons-material/OpenInFull'; +import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap'; interface CoveredAreaMapProps { boundingBox?: LatLngExpression[]; @@ -158,13 +158,17 @@ const CoveredAreaMap: React.FC = ({ if (displayGtfsVisualizationView) { return ( <> - - - - - + + + + - ) + ); } return ( @@ -192,80 +196,87 @@ const CoveredAreaMap: React.FC = ({ }, minHeight: '50vh', }} - title={mapDisplayError ? '' : t('coveredAreaTitle') + ' - ' + t(view)} + title={''} width={{ xs: '100%' }} outlineColor={theme.palette.primary.dark} padding={2} - action={ - <> + > + {view === 'gtfsVisualizationView' && ( - + )} - {feed?.data_type === 'gbfs' ? ( - - {latestAutodiscoveryUrl != undefined && ( - - )} - {(geoJsonData as GeoJSONDataGBFS)?.extracted_at != undefined && ( - - {t('common:updated')}:{' '} - {displayFormattedDate( - (geoJsonData as GeoJSONDataGBFS).extracted_at, - )} - - )} - - ) : ( - - - - - - - - - - - - - - - - - - )} - - } - > + {feed?.data_type === 'gbfs' ? ( + + {latestAutodiscoveryUrl != undefined && ( + + )} + {(geoJsonData as GeoJSONDataGBFS)?.extracted_at != undefined && ( + + {t('common:updated')}:{' '} + {displayFormattedDate( + (geoJsonData as GeoJSONDataGBFS).extracted_at, + )} + + )} + + ) : ( + + + + + + + + + + + + + + + + + + )} + {feed?.data_type === 'gtfs' && boundingBox === undefined && view === 'boundingBoxView' && ( diff --git a/web-app/src/app/components/GtfsVisualizationMap.tsx b/web-app/src/app/components/GtfsVisualizationMap.tsx index a0710b2c9..5179d1ee1 100644 --- a/web-app/src/app/components/GtfsVisualizationMap.tsx +++ b/web-app/src/app/components/GtfsVisualizationMap.tsx @@ -129,7 +129,6 @@ export const GtfsVisualizationMap = ({ return ( - {/* setFilteredRoutes(val)}> */} { - const a = ['in', Number(id), ['get', 'route_ids']]; - return a as any; + return ['in', `\"${id}\"`, ['get', 'route_ids']] as any; // VERY IMPORTANT: during the conversion to PMTiles, the route_ids are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" }), ], [ 'any', - ['in', '141', ['get', 'route_ids']], ...hoverInfo.map((id) => { - const a = ['in', Number(id), ['get', 'route_ids']]; - return a as any; + return ['in', `\"${id}\"`, ['get', 'route_ids']] as any; }), ], ], diff --git a/web-app/src/app/components/RouteSelector.tsx b/web-app/src/app/components/RouteSelector.tsx index 4a4d9ebbe..b4068477c 100644 --- a/web-app/src/app/components/RouteSelector.tsx +++ b/web-app/src/app/components/RouteSelector.tsx @@ -1,29 +1,28 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; import { - Drawer, TextField, List, - ListItem, Checkbox, ListItemText, ListItemIcon, Typography, Box, + ListItemButton, } from '@mui/material'; interface RouteSelectorProps { routes: Array; // Replace 'any' with a more specific type if available + selectedRouteIds?: string[]; onSelectionChange?: (selectedRoutes: string[]) => void; - open?: boolean; } export default function RouteSelector({ routes, + selectedRouteIds = [], onSelectionChange, - open = true, }: RouteSelectorProps) { const [search, setSearch] = useState(''); - const [selectedRoutes, setSelectedRoutes] = useState([]); + const [selectedRoutes, setSelectedRoutes] = useState(selectedRouteIds); const filteredRoutes = useMemo(() => { const searchLower = search.toLowerCase(); @@ -34,6 +33,10 @@ export default function RouteSelector({ ); }, [search, routes]); + useEffect(() => { + setSelectedRoutes([...selectedRouteIds]); + }, [selectedRouteIds]); + const handleToggle = (routeId: string) => { setSelectedRoutes((prev) => { const newSelection = prev.includes(routeId) @@ -46,11 +49,7 @@ export default function RouteSelector({ }; return ( - - - {' '} - Select Routes{' '} - + setSearch(e.target.value)} - sx={{ mb: 2 }} /> - - {filteredRoutes.map((route) => ( - + {filteredRoutes.sort((a,b) => (a.routeId - b.routeId)).map((route) => ( + handleToggle(route.routeId)} + dense={true} > - + - {route.routeName} + {route.routeId} - {route.routeName} + } /> - + ))} diff --git a/web-app/src/app/screens/Feed/components/FeedSummary.tsx b/web-app/src/app/screens/Feed/components/FeedSummary.tsx index 1737b07f0..60f126faa 100644 --- a/web-app/src/app/screens/Feed/components/FeedSummary.tsx +++ b/web-app/src/app/screens/Feed/components/FeedSummary.tsx @@ -43,6 +43,9 @@ import { StyledListItem, } from '../Feed.styles'; import FeedAuthenticationSummaryInfo from './FeedAuthenticationSummaryInfo'; +import CommuteIcon from '@mui/icons-material/Commute'; +import { Link as RouterLink } from 'react-router-dom'; +import TravelExploreIcon from '@mui/icons-material/TravelExplore'; export interface FeedSummaryProps { feed: GTFSFeedType | GTFSRTFeedType | undefined; @@ -150,7 +153,44 @@ export default function FeedSummary({ 500 - + + + + + + + + )} + {feed?.data_type === 'gtfs' && ( + + + {/* TODO: get back to this */} + + + Type of Routes + + + + Bus, Subway + + + + + )} diff --git a/web-app/src/app/screens/Feed/components/FullMapView.tsx b/web-app/src/app/screens/Feed/components/FullMapView.tsx index 837883717..30e16eb99 100644 --- a/web-app/src/app/screens/Feed/components/FullMapView.tsx +++ b/web-app/src/app/screens/Feed/components/FullMapView.tsx @@ -1,4 +1,4 @@ -import { Box, Fab } from '@mui/material'; +import { Box, Fab, Typography, Button, Chip, useTheme } from '@mui/material'; import RouteSelector from '../../../components/RouteSelector'; import sampleRoutes from './sample-route-output.json'; import React, { useState } from 'react'; @@ -8,13 +8,23 @@ import NestedCheckboxList, { type CheckboxStructure, } from '../../../components/NestedCheckboxList'; import { routeTypesMapping } from '../../../constants/RouteTypes'; +import { ChevronLeft } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; +import { SearchHeader } from '../../../styles/Filters.styles'; export interface FullMapViewProps {} export default function FullMapView({}: FullMapViewProps): React.ReactElement { + const { t } = useTranslation('feeds'); + const theme = useTheme(); const [filteredRoutes, setFilteredRoutes] = useState([]); const [filteredRouteTypes, setFilteredRouteTypes] = useState([]); + const clearAllFilters = () => { + setFilteredRoutes([]); + setFilteredRouteTypes([]); + }; + const getUniqueRouteTypesCheckboxData = ( routes: Array<{ routeType: string }>, ): CheckboxStructure[] => { @@ -47,19 +57,55 @@ export default function FullMapView({}: FullMapViewProps): React.ReactElement { height: '100%', position: 'relative', display: 'flex', + pt: 1, + minHeight: 'calc(100vh - 64px - 36px)', // Adjusts for the height of the header and any additional padding + mt: { xs: -2, md: -4 }, // Adjusts for the margin of the header }} > - { - window.history.back(); - }} - > - - - + + + + + + + Route Types + + { + setFilteredRouteTypes( + checkboxData + .map((item) => { + return item.checked ? item?.props?.routeTypeId ?? '' : ''; + }) + .filter((item) => item !== ''), + ); + }} + /> + + + Routes + + { + setFilteredRoutes(val); + }} + > + + Visibility + - {/* Route type selector */} - - { - setFilteredRouteTypes( - checkboxData - .map((item) => { - return item.checked ? item?.props?.routeTypeId ?? '' : ''; - }) - .filter((item) => item !== ''), - ); - }} - /> + + + + {filteredRoutes.map((routeId) => ( + { + setFilteredRoutes((prev) => + prev.filter((id) => id !== routeId), + ); + }} + sx={{ cursor: 'pointer' }} + > + ))} + {filteredRoutes.length > 0 && ( + + )} - { - setFilteredRoutes(val); + - - - + > + { + window.history.back(); + }} + > + + + + ); diff --git a/web-app/src/app/screens/Feed/index.tsx b/web-app/src/app/screens/Feed/index.tsx index c0f42dfbb..1d2d5d580 100644 --- a/web-app/src/app/screens/Feed/index.tsx +++ b/web-app/src/app/screens/Feed/index.tsx @@ -340,12 +340,12 @@ export default function Feed(): React.ReactElement { feedId, structuredData, - - + + ({ - '&.no-collapse': { - margin: '12px 0', - }, - '&:not(:first-of-type)': { - marginTop: theme.spacing(1), - }, - '&:after': { - content: '""', - display: 'block', - height: '3px', - width: '104px', - background: theme.palette.text.primary, - }, -})); - export const chipHolderStyles: SxProps = (theme) => ({ display: 'flex', flexWrap: 'wrap', diff --git a/web-app/src/app/screens/Feeds/SearchFilters.tsx b/web-app/src/app/screens/Feeds/SearchFilters.tsx index 3e0cd5f0c..c1e4f2c7e 100644 --- a/web-app/src/app/screens/Feeds/SearchFilters.tsx +++ b/web-app/src/app/screens/Feeds/SearchFilters.tsx @@ -2,13 +2,13 @@ import { Accordion, AccordionSummary, AccordionDetails } from '@mui/material'; import NestedCheckboxList, { type CheckboxStructure, } from '../../components/NestedCheckboxList'; -import { SearchHeader } from './Feeds.styles'; import { useTranslation } from 'react-i18next'; import { useRemoteConfig } from '../../context/RemoteConfigProvider'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { useEffect, useState } from 'react'; import { DATASET_FEATURES, groupFeaturesByComponent } from '../../utils/consts'; import { type GbfsVersionConfig } from '../../interface/RemoteConfig'; +import { SearchHeader } from '../../styles/Filters.styles'; function setInitialExpandGroup(): Record { const expandGroup: Record = {}; diff --git a/web-app/src/app/styles/Filters.styles.ts b/web-app/src/app/styles/Filters.styles.ts new file mode 100644 index 000000000..54840913b --- /dev/null +++ b/web-app/src/app/styles/Filters.styles.ts @@ -0,0 +1,17 @@ +import { styled, Typography } from "@mui/material"; + +export const SearchHeader = styled(Typography)(({ theme }) => ({ + '&.no-collapse': { + margin: '12px 0', + }, + '&:not(:first-of-type)': { + marginTop: theme.spacing(1), + }, + '&:after': { + content: '""', + display: 'block', + height: '3px', + width: '104px', + background: theme.palette.text.primary, + }, +})); \ No newline at end of file From edc758fe5a12fe7e0a0e50539fdb3857b633a11f Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Thu, 19 Jun 2025 08:37:29 -0400 Subject: [PATCH 04/16] stop colors poc --- .../app/components/GtfsVisualizationMap.tsx | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/web-app/src/app/components/GtfsVisualizationMap.tsx b/web-app/src/app/components/GtfsVisualizationMap.tsx index 5179d1ee1..6817c5245 100644 --- a/web-app/src/app/components/GtfsVisualizationMap.tsx +++ b/web-app/src/app/components/GtfsVisualizationMap.tsx @@ -1,6 +1,9 @@ import { useEffect, useRef, useState } from 'react'; import Map, { type MapRef, MapProvider } from 'react-map-gl/maplibre'; -import maplibregl, { type LngLatBoundsLike } from 'maplibre-gl'; +import maplibregl, { + ExpressionSpecification, + type LngLatBoundsLike, +} from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; import { Protocol } from 'pmtiles'; import { type LatLngExpression } from 'leaflet'; @@ -126,6 +129,26 @@ export const GtfsVisualizationMap = ({ ], }; + // TODO: Get the route color data from the route selector + const testSelectedRouteColors = ['00B300', '009EE0']; // Test colors: green line and bus line (blue) + + // The Types for these types of expressions are not well defined in the maplibre-gl types, so we use 'any' here + // Should revisit the typing situation in the future + function generateColorMatchExpression( + colors: string[], + propertyName = 'route_colors', + fallback = '#888888', + ) { + const expression: any[] = ['case']; + + colors.forEach((color) => { + expression.push(['in', `"${color}"`, ['get', propertyName]], `#${color}`); + }); + + expression.push(fallback); // Default color if none match + return expression; + } + return ( @@ -282,7 +305,7 @@ export const GtfsVisualizationMap = ({ 3, // Default width ], }, - + filter: [ 'any', ['in', ['get', 'route_id'], ['literal', hoverInfo]], @@ -295,17 +318,17 @@ export const GtfsVisualizationMap = ({ 'source-layer': 'stopsoutput', // Name of the geojson file when converting to pmtile. stops-output.geojson -> stopssoutput type: 'circle', paint: { - 'circle-radius': 6, - // 'circle-color': [ - // 'concat', - // '#', - // ['at', 0, ['get', 'route_colors']], - // ], // Red highlight - 'circle-color': '#0095E6', + 'circle-radius': 8, + 'circle-color': generateColorMatchExpression( + testSelectedRouteColors, + 'route_colors', + '#000000', + ) as unknown as ExpressionSpecification, // VERY IMPORTANT: during the conversion to PMTiles, the route_colors are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" 'circle-opacity': 0.8, }, - filter: - [ + minzoom: 10, + maxzoom: 22, + filter: [ 'any', ['in', ['get', 'stop_id'], ['literal', hoverInfo]], [ From 5edea6992f877f8b7c840cb21b421a6356bb9520 Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Thu, 19 Jun 2025 09:03:31 -0400 Subject: [PATCH 05/16] scale and compass to map --- .../app/components/GtfsVisualizationMap.tsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/web-app/src/app/components/GtfsVisualizationMap.tsx b/web-app/src/app/components/GtfsVisualizationMap.tsx index 6817c5245..fbdb162dd 100644 --- a/web-app/src/app/components/GtfsVisualizationMap.tsx +++ b/web-app/src/app/components/GtfsVisualizationMap.tsx @@ -1,5 +1,10 @@ import { useEffect, useRef, useState } from 'react'; -import Map, { type MapRef, MapProvider } from 'react-map-gl/maplibre'; +import Map, { + type MapRef, + MapProvider, + NavigationControl, + ScaleControl, +} from 'react-map-gl/maplibre'; import maplibregl, { ExpressionSpecification, type LngLatBoundsLike, @@ -347,7 +352,22 @@ export const GtfsVisualizationMap = ({ }, ], }} - > + > + {/* TODO: Idea only display the compass and scale on advanced map view */} + + + From 8f92c2674ecb084a407058693d8ff7cfb14c4e9e Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Thu, 19 Jun 2025 14:23:07 -0400 Subject: [PATCH 06/16] click poc --- .../app/components/GtfsVisualizationMap.tsx | 65 ++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/web-app/src/app/components/GtfsVisualizationMap.tsx b/web-app/src/app/components/GtfsVisualizationMap.tsx index fbdb162dd..a3346bd96 100644 --- a/web-app/src/app/components/GtfsVisualizationMap.tsx +++ b/web-app/src/app/components/GtfsVisualizationMap.tsx @@ -4,6 +4,7 @@ import Map, { MapProvider, NavigationControl, ScaleControl, + Popup, } from 'react-map-gl/maplibre'; import maplibregl, { ExpressionSpecification, @@ -13,7 +14,7 @@ import 'maplibre-gl/dist/maplibre-gl.css'; import { Protocol } from 'pmtiles'; import { type LatLngExpression } from 'leaflet'; import type { FeatureCollection } from 'geojson'; -import { Box, useTheme } from '@mui/material'; +import { Box, Popover, useTheme } from '@mui/material'; import { MapElement } from './MapElement'; export interface GtfsVisualizationMapProps { @@ -31,8 +32,43 @@ export const GtfsVisualizationMap = ({ const [hoverInfo, setHoverInfo] = useState([]); const [hoverData, setHoverData] = useState(''); const [mapElement, setMapElement] = useState([]); + const [mapClickData, setMapClickData] = useState< + Record + >({}); + const [anchorPosition, setAnchorPosition] = useState<{ + left: number; + top: number; + } | null>(null); const mapRef = useRef(null); + const handleMouseClick = (event: maplibregl.MapLayerMouseEvent): void => { + const map = mapRef.current?.getMap(); + if (map != undefined) { + // Get the features under the mouse pointer + const { offsetLeft, offsetTop } = mapRef.current!.getContainer(); + const features = map.queryRenderedFeatures(event.point, { + layers: ['stops', 'routes'], // Change this to your actual layer ID + }); + setMapClickData({ + ...(features[0]?.properties || {}), + longitude: event.lngLat.lng, + latitude: event.lngLat.lat, + }); // Example properties, adjust as needed + console.log('Mouse clicked on map:', features); + setAnchorPosition({ + left: event.point.x + offsetLeft, + top: event.point.y + offsetTop, + }); + } + }; + + const handleClose = () => { + setMapClickData({}); + setAnchorPosition(null); + }; + + console.log('this is it mapClickData', mapClickData); + const handleMouseMove = (event: maplibregl.MapLayerMouseEvent): void => { // Ensure that the mapRef is not null before trying to access the map const map = mapRef.current?.getMap(); @@ -169,7 +205,9 @@ export const GtfsVisualizationMap = ({ > { + handleMouseClick(event); + }} ref={mapRef} onMouseMove={(event) => { handleMouseMove(event); @@ -367,6 +405,29 @@ export const GtfsVisualizationMap = ({ 'rgba(0, 0, 0, 0.2) 0px 3px 5px -1px, rgba(0, 0, 0, 0.14) 0px 6px 10px 0px, rgba(0, 0, 0, 0.12) 0px 1px 18px 0px', }} /> + + {Object.keys(mapClickData).length > 0 && ( + setMapClickData({})} + closeOnClick={false} + > +
+ Clicked at: +
+ {Number(mapClickData.latitude).toFixed(5)},{' '} + {Number(mapClickData.longitude).toFixed(5)} +
+ {Object.entries(mapClickData).map(([key, value]) => ( +

+ {key} - {value} +

+ ))} +
+
+ )}
From d28d983f6512e5539c917a59940a8a12e421c1e4 Mon Sep 17 00:00:00 2001 From: cka-y Date: Fri, 20 Jun 2025 09:34:17 -0400 Subject: [PATCH 07/16] feat: filtering func on full map view --- .../app/components/GtfsVisualizationMap.tsx | 112 +++++++++++++----- web-app/src/app/constants/RouteTypes.ts | 4 + .../screens/Feed/components/FullMapView.tsx | 69 +++++++++-- 3 files changed, 142 insertions(+), 43 deletions(-) diff --git a/web-app/src/app/components/GtfsVisualizationMap.tsx b/web-app/src/app/components/GtfsVisualizationMap.tsx index a3346bd96..f8b3bbf82 100644 --- a/web-app/src/app/components/GtfsVisualizationMap.tsx +++ b/web-app/src/app/components/GtfsVisualizationMap.tsx @@ -1,3 +1,5 @@ +/* eslint-disable */ + import { useEffect, useRef, useState } from 'react'; import Map, { type MapRef, @@ -7,26 +9,29 @@ import Map, { Popup, } from 'react-map-gl/maplibre'; import maplibregl, { - ExpressionSpecification, + type ExpressionSpecification, type LngLatBoundsLike, } from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; import { Protocol } from 'pmtiles'; import { type LatLngExpression } from 'leaflet'; import type { FeatureCollection } from 'geojson'; -import { Box, Popover, useTheme } from '@mui/material'; +import { Box, useTheme } from '@mui/material'; import { MapElement } from './MapElement'; +import { routeTypesMapping } from "../constants/RouteTypes"; export interface GtfsVisualizationMapProps { polygon: LatLngExpression[]; filteredRoutes?: string[]; filteredRouteTypes?: string[]; + hideStops?: boolean; } export const GtfsVisualizationMap = ({ polygon, filteredRoutes = [], filteredRouteTypes = [], + hideStops = false, }: GtfsVisualizationMapProps): JSX.Element => { const theme = useTheme(); const [hoverInfo, setHoverInfo] = useState([]); @@ -41,6 +46,45 @@ export const GtfsVisualizationMap = ({ } | null>(null); const mapRef = useRef(null); + const reversedRouteTypesMapping = Object.fromEntries( + Object.entries(routeTypesMapping).map(([k, v]) => [v, k]) + ); + const filteredRouteTypesIds = filteredRouteTypes.map(d => reversedRouteTypesMapping[d]); + + // Create a map to store routeId to routeColor mapping + const routeIdToColorMap: Record = {}; + mapElement.forEach((el) => { + if (!el.isStop && el.routeId && el.routeColor) { + routeIdToColorMap[el.routeId] = el.routeColor; + } + }); + function generateStopColorExpression( + routeIdToColor: Record, + fallback = '#888' + ): ExpressionSpecification { + const expression: any[] = ['case']; + + Object.entries(routeIdToColor).forEach(([routeId, color]) => { + expression.push( + ['in', `"${routeId}"`, ['get', 'route_ids']], + `#${color}` + ); + }); + + // If no conditions added, just return a fallback color directly + if (expression.length === 1) { + return fallback as unknown as ExpressionSpecification; + } + + expression.push(fallback); // Add fallback color + return expression as ExpressionSpecification; + } + + const routeTypeFilter: ExpressionSpecification | boolean = + filteredRouteTypes.length > 0 + ? ['in', ['get', 'route_type'], ['literal', filteredRouteTypesIds]] + : true; // if no filter applied, show all + const handleMouseClick = (event: maplibregl.MapLayerMouseEvent): void => { const map = mapRef.current?.getMap(); if (map != undefined) { @@ -170,26 +214,6 @@ export const GtfsVisualizationMap = ({ ], }; - // TODO: Get the route color data from the route selector - const testSelectedRouteColors = ['00B300', '009EE0']; // Test colors: green line and bus line (blue) - - // The Types for these types of expressions are not well defined in the maplibre-gl types, so we use 'any' here - // Should revisit the typing situation in the future - function generateColorMatchExpression( - colors: string[], - propertyName = 'route_colors', - fallback = '#888888', - ) { - const expression: any[] = ['case']; - - colors.forEach((color) => { - expression.push(['in', `"${color}"`, ['get', propertyName]], `#${color}`); - }); - - expression.push(fallback); // Default color if none match - return expression; - } - return ( @@ -253,10 +277,10 @@ export const GtfsVisualizationMap = ({ minzoom: 0, maxzoom: 22, }, - { id: 'routes-white', // white padding on the route lines source: 'routes', + filter: routeTypeFilter, 'source-layer': 'routesoutput', // Name of the geojson file when converting to pmtile. route-output.geojson -> routesoutput type: 'line', paint: { @@ -275,6 +299,7 @@ export const GtfsVisualizationMap = ({ }, { id: 'routes', + filter: routeTypeFilter, source: 'routes', 'source-layer': 'routesoutput', // Name of the geojson file when converting to pmtile. route-output.geojson -> routesoutput type: 'line', @@ -293,6 +318,8 @@ export const GtfsVisualizationMap = ({ ], 'line-opacity': 0.4, // Opacity of the route lines }, + // If routeTypesFilter includes a value -> show that + // If routeTypesFilter is empty -> show all layout: { 'line-sort-key': [ 'match', @@ -317,6 +344,7 @@ export const GtfsVisualizationMap = ({ // }, { id: 'stops', + filter: !hideStops, // Hide stops if hideStops is true source: 'sample', 'source-layer': 'stopsoutput', // Name of the geojson file when converting to pmtile. stops-output.geojson -> stopssoutput type: 'circle', @@ -361,17 +389,13 @@ export const GtfsVisualizationMap = ({ 'source-layer': 'stopsoutput', // Name of the geojson file when converting to pmtile. stops-output.geojson -> stopssoutput type: 'circle', paint: { - 'circle-radius': 8, - 'circle-color': generateColorMatchExpression( - testSelectedRouteColors, - 'route_colors', - '#000000', - ) as unknown as ExpressionSpecification, // VERY IMPORTANT: during the conversion to PMTiles, the route_colors are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" + 'circle-radius': 6, + 'circle-color': generateStopColorExpression(routeIdToColorMap) as ExpressionSpecification, // VERY IMPORTANT: during the conversion to PMTiles, the route_colors are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" 'circle-opacity': 0.8, }, minzoom: 10, maxzoom: 22, - filter: [ + filter: hideStops ? !hideStops : [ 'any', ['in', ['get', 'stop_id'], ['literal', hoverInfo]], [ @@ -388,6 +412,33 @@ export const GtfsVisualizationMap = ({ ], ], }, + { + id: 'stops-highlight-outer', + source: 'sample', + 'source-layer': 'stopsoutput', + type: 'circle', + paint: { + 'circle-radius': 3, + 'circle-color': '#ffffff', + 'circle-opacity': 1, + }, + filter: hideStops ? !hideStops : [ + 'any', + ['in', ['get', 'stop_id'], ['literal', hoverInfo]], + [ + 'any', + ...filteredRoutes.map((id) => { + return ['in', `\"${id}\"`, ['get', 'route_ids']] as any; // VERY IMPORTANT: during the conversion to PMTiles, the route_ids are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" + }), + ], + [ + 'any', + ...hoverInfo.map((id) => { + return ['in', `\"${id}\"`, ['get', 'route_ids']] as any; + }), + ], + ], + } ], }} > @@ -406,6 +457,7 @@ export const GtfsVisualizationMap = ({ }} /> + {/*TODO: could be own component*/} {Object.keys(mapClickData).length > 0 && ( = { '12': 'High Speed Train', '13': 'Airplane', }; + +export const reversedRouteTypesMapping = Object.fromEntries( + Object.entries(routeTypesMapping).map(([k, v]) => [v, k]), +); diff --git a/web-app/src/app/screens/Feed/components/FullMapView.tsx b/web-app/src/app/screens/Feed/components/FullMapView.tsx index 30e16eb99..1bc8bb640 100644 --- a/web-app/src/app/screens/Feed/components/FullMapView.tsx +++ b/web-app/src/app/screens/Feed/components/FullMapView.tsx @@ -1,4 +1,4 @@ -import { Box, Fab, Typography, Button, Chip, useTheme } from '@mui/material'; +import { Box, Fab, Button, Chip, useTheme } from '@mui/material'; import RouteSelector from '../../../components/RouteSelector'; import sampleRoutes from './sample-route-output.json'; import React, { useState } from 'react'; @@ -12,17 +12,22 @@ import { ChevronLeft } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; import { SearchHeader } from '../../../styles/Filters.styles'; -export interface FullMapViewProps {} - -export default function FullMapView({}: FullMapViewProps): React.ReactElement { +export default function FullMapView(): React.ReactElement { const { t } = useTranslation('feeds'); const theme = useTheme(); const [filteredRoutes, setFilteredRoutes] = useState([]); const [filteredRouteTypes, setFilteredRouteTypes] = useState([]); + const [hideStops, setHideStops] = useState(false); - const clearAllFilters = () => { + const clearAllFilters = (): void => { setFilteredRoutes([]); setFilteredRouteTypes([]); + setHideStops(false); + }; + + const getRouteDisplayName = (routeId: string): string => { + const route = sampleRoutes.find((r) => r.routeId === routeId); + return route != null ? `${route.routeName} [${route.routeId}]` : routeId; }; const getUniqueRouteTypesCheckboxData = ( @@ -36,7 +41,7 @@ export default function FullMapView({}: FullMapViewProps): React.ReactElement { }); return Array.from(uniqueTypes).map((type) => ({ title: routeTypesMapping[type], - checked: true, + checked: filteredRouteTypes.includes(routeTypesMapping[type]), props: { routeTypeId: type, }, @@ -44,6 +49,7 @@ export default function FullMapView({}: FullMapViewProps): React.ReactElement { })) as CheckboxStructure[]; }; + // TODO: this is hardcoded for Montreal, should be dynamic const bb = [ [45.402668, -73.956204], [45.402668, -73.480581], @@ -86,7 +92,7 @@ export default function FullMapView({}: FullMapViewProps): React.ReactElement { setFilteredRouteTypes( checkboxData .map((item) => { - return item.checked ? item?.props?.routeTypeId ?? '' : ''; + return item.checked ? item?.title ?? '' : ''; }) .filter((item) => item !== ''), ); @@ -110,16 +116,23 @@ export default function FullMapView({}: FullMapViewProps): React.ReactElement { checkboxData={[ { title: 'Hide Stops', - checked: false, + checked: hideStops, type: 'checkbox', }, ]} onCheckboxChange={(checkboxData: CheckboxStructure[]) => { - console.log(checkboxData); + setHideStops(checkboxData[0].checked); }} /> - + + {hideStops && ( + { + setHideStops(false); + }} + sx={{ cursor: 'pointer' }} + > + )} + {filteredRouteTypes.map((routeType) => ( + { + setFilteredRouteTypes((prev) => + prev.filter((type) => type !== routeType), + ); + }} + sx={{ cursor: 'pointer' }} + > + ))} {filteredRoutes.map((routeId) => ( { setFilteredRoutes((prev) => prev.filter((id) => id !== routeId), @@ -143,7 +183,10 @@ export default function FullMapView({}: FullMapViewProps): React.ReactElement { sx={{ cursor: 'pointer' }} > ))} - {filteredRoutes.length > 0 && ( + + {(filteredRoutes.length > 0 || + filteredRouteTypes.length > 0 || + hideStops) && ( + )} + {hideStops && ( + { + setHideStops(false); + }} + sx={{ cursor: 'pointer' }} + > + )} + {filteredRouteTypes.map((routeType) => ( + { + setFilteredRouteTypes((prev) => + prev.filter((type) => type !== routeType), + ); + }} + sx={{ cursor: 'pointer' }} + > + ))} + {filteredRoutes.map((routeId) => ( + { + setFilteredRoutes((prev) => prev.filter((id) => id !== routeId)); + }} + sx={{ cursor: 'pointer' }} + > + ))} + + ); + }; + return ( - - + + + + {renderFilterChips()} + @@ -163,7 +245,23 @@ export default function FullMapView(): React.ReactElement { setFilteredRoutes(val); }} > - + + + + - - {hideStops && ( - { - setHideStops(false); - }} - sx={{ cursor: 'pointer' }} - > - )} - {filteredRouteTypes.map((routeType) => ( - { - setFilteredRouteTypes((prev) => - prev.filter((type) => type !== routeType), - ); - }} - sx={{ cursor: 'pointer' }} - > - ))} - {filteredRoutes.map((routeId) => ( - { - setFilteredRoutes((prev) => - prev.filter((id) => id !== routeId), - ); - }} - sx={{ cursor: 'pointer' }} - > - ))} - - {(filteredRoutes.length > 0 || - filteredRouteTypes.length > 0 || - hideStops) && ( - - )} - + {renderFilterChips()} { window.history.back(); @@ -257,6 +293,20 @@ export default function FullMapView(): React.ReactElement { > + setShowMapControlMobile(!showMapControlMobile)} + > + + Date: Thu, 26 Jun 2025 09:07:27 -0400 Subject: [PATCH 12/16] mobile adjustments --- web-app/src/app/screens/Feed/Map.styles.ts | 2 +- web-app/src/app/screens/Feed/components/FullMapView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web-app/src/app/screens/Feed/Map.styles.ts b/web-app/src/app/screens/Feed/Map.styles.ts index 0b44b3d25..50db9b01c 100644 --- a/web-app/src/app/screens/Feed/Map.styles.ts +++ b/web-app/src/app/screens/Feed/Map.styles.ts @@ -8,7 +8,7 @@ export const StyledMapControlPanel = styled(Box, { shouldForwardProp: (prop) => prop !== 'showMapControlMobile', })(({ theme, showMapControlMobile }) => ({ padding: theme.spacing(2), - paddingTop: '100px', // to account for the fixed header + paddingTop: '100px', // to account for the fixed header on mobile flexDirection: 'column', flexWrap: 'nowrap', backgroundColor: theme.palette.background.default, diff --git a/web-app/src/app/screens/Feed/components/FullMapView.tsx b/web-app/src/app/screens/Feed/components/FullMapView.tsx index a1e9cd993..903fd4295 100644 --- a/web-app/src/app/screens/Feed/components/FullMapView.tsx +++ b/web-app/src/app/screens/Feed/components/FullMapView.tsx @@ -299,7 +299,7 @@ export default function FullMapView(): React.ReactElement { top: 10, right: 70, zIndex: 1000, - display: { xs: 'block', md: 'none' }, + display: { xs: 'inline-flex', md: 'none' }, }} size='small' aria-label='filter' From e6cd3f714be63ec9146f2a481c2990c94c4bbf2b Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Thu, 26 Jun 2025 14:41:43 -0400 Subject: [PATCH 13/16] routes and stops hover and click improvements --- .../app/components/GtfsVisualizationMap.tsx | 313 ++++++++++++------ .../src/app/components/Map/MapDataPopup.tsx | 169 ++++++++++ web-app/src/app/components/MapElement.tsx | 154 +++++---- web-app/src/app/constants/RouteTypes.ts | 17 - web-app/src/app/constants/RouteTypes.tsx | 48 +++ web-app/src/app/screens/Feed/Map.styles.ts | 10 +- .../screens/Feed/components/FullMapView.tsx | 36 +- 7 files changed, 545 insertions(+), 202 deletions(-) create mode 100644 web-app/src/app/components/Map/MapDataPopup.tsx delete mode 100644 web-app/src/app/constants/RouteTypes.ts create mode 100644 web-app/src/app/constants/RouteTypes.tsx diff --git a/web-app/src/app/components/GtfsVisualizationMap.tsx b/web-app/src/app/components/GtfsVisualizationMap.tsx index 54d18ff2d..5044be5b2 100644 --- a/web-app/src/app/components/GtfsVisualizationMap.tsx +++ b/web-app/src/app/components/GtfsVisualizationMap.tsx @@ -6,7 +6,6 @@ import Map, { MapProvider, NavigationControl, ScaleControl, - Popup, } from 'react-map-gl/maplibre'; import maplibregl, { type ExpressionSpecification, @@ -17,8 +16,12 @@ import { Protocol } from 'pmtiles'; import { type LatLngExpression } from 'leaflet'; import type { FeatureCollection } from 'geojson'; import { Box, useTheme } from '@mui/material'; -import { MapElement } from './MapElement'; -import { routeTypesMapping } from "../constants/RouteTypes"; +import { MapElement, MapRouteElement, MapStopElement } from './MapElement'; +import { + reversedRouteTypesMapping, + RouteTypeMetadata, +} from '../constants/RouteTypes'; +import { MapDataPopup } from './Map/MapDataPopup'; export interface GtfsVisualizationMapProps { polygon: LatLngExpression[]; @@ -37,37 +40,41 @@ export const GtfsVisualizationMap = ({ const [hoverInfo, setHoverInfo] = useState([]); const [hoverData, setHoverData] = useState(''); const [mapElement, setMapElement] = useState([]); - const [mapClickData, setMapClickData] = useState< - Record - >({}); - const [anchorPosition, setAnchorPosition] = useState<{ - left: number; - top: number; - } | null>(null); + const [mapClickRouteData, setMapClickRouteData] = useState | null>(null); + const [mapClickStopData, setMapClickStopData] = useState | null>(null); const mapRef = useRef(null); - const reversedRouteTypesMapping = Object.fromEntries( - Object.entries(routeTypesMapping).map(([k, v]) => [v, k]) + const filteredRouteTypesIds = filteredRouteTypes.map( + (d) => reversedRouteTypesMapping[d], ); - const filteredRouteTypesIds = filteredRouteTypes.map(d => reversedRouteTypesMapping[d]); // Create a map to store routeId to routeColor mapping const routeIdToColorMap: Record = {}; mapElement.forEach((el) => { - if (!el.isStop && el.routeId && el.routeColor) { - routeIdToColorMap[el.routeId] = el.routeColor; + if (!el.isStop) { + const routeElement: MapRouteElement = el as MapRouteElement; + if (routeElement.routeId && routeElement.routeColor) { + routeIdToColorMap[routeElement.routeId] = routeElement.routeColor; + } } }); + function generateStopColorExpression( routeIdToColor: Record, - fallback = '#888' + fallback = '#888', ): ExpressionSpecification { const expression: any[] = ['case']; Object.entries(routeIdToColor).forEach(([routeId, color]) => { expression.push( ['in', `"${routeId}"`, ['get', 'route_ids']], - `#${color}` + `#${color}`, ); }); @@ -89,30 +96,44 @@ export const GtfsVisualizationMap = ({ const map = mapRef.current?.getMap(); if (map != undefined) { // Get the features under the mouse pointer - const { offsetLeft, offsetTop } = mapRef.current!.getContainer(); const features = map.queryRenderedFeatures(event.point, { - layers: ['stops', 'routes'], // Change this to your actual layer ID - }); - setMapClickData({ - ...(features[0]?.properties || {}), - longitude: event.lngLat.lng, - latitude: event.lngLat.lat, - }); // Example properties, adjust as needed - console.log('Mouse clicked on map:', features); - setAnchorPosition({ - left: event.point.x + offsetLeft, - top: event.point.y + offsetTop, + layers: ['stops-highlight', 'routes-highlight'], // Change this to your actual layer ID }); + + const selectedStop = features.find( + (feature) => feature.layer.id === 'stops-highlight', + ); + if (selectedStop != undefined) { + setMapClickStopData({ + ...selectedStop.properties, + longitude: String(event.lngLat.lng), + latitude: String(event.lngLat.lat), + }); // Example properties, adjust as needed + console.log('Mouse selectedStop', selectedStop); + setMapClickRouteData(null); + return; + } + + const selectedRoute = features.find( + (feature) => feature.layer.id === 'routes-highlight', + ); + if (selectedRoute != undefined) { + setMapClickRouteData({ + ...selectedRoute.properties, + longitude: String(event.lngLat.lng), + latitude: String(event.lngLat.lat), + }); // Example properties, adjust as needed + console.log('Mouse clicked on map:', features); + setMapClickStopData(null); + } } }; - const handleClose = () => { - setMapClickData({}); - setAnchorPosition(null); + const handlePopupClose = () => { + setMapClickRouteData(null); + setMapClickStopData(null); }; - //console.log('this is it mapClickData', mapClickData); - const handleMouseMove = (event: maplibregl.MapLayerMouseEvent): void => { // Ensure that the mapRef is not null before trying to access the map const map = mapRef.current?.getMap(); @@ -121,15 +142,44 @@ export const GtfsVisualizationMap = ({ if (map != undefined) { // Get the features under the mouse pointer const features = map.queryRenderedFeatures(event.point, { - layers: ['stops', 'routes', 'routes-white'], // Change this to your actual layer ID + layers: ['stops', 'routes', 'routes-white', 'stops-highlight'], // Change this to your actual layer ID }); - if (features.length > 0) { + if ( + features.length > 0 || + mapClickRouteData != null || + mapClickStopData != null + ) { + if (mapClickRouteData != null) { + const mapElement: MapRouteElement = { + isStop: false, + name: mapClickRouteData.route_long_name, + routeType: Number(mapClickRouteData.route_type), + routeColor: mapClickRouteData.route_color, + routeTextColor: mapClickRouteData.route_text_color, + routeId: mapClickRouteData.route_id, + }; + mapElements.push(mapElement); + } + if (mapClickStopData != null) { + const mapElement: MapStopElement = { + isStop: true, + name: mapClickStopData.stop_name, + locationType: Number(mapClickStopData.location_type), + stopId: mapClickStopData.stop_id, + }; + mapElements.push(mapElement); + } features.forEach((feature) => { - if (feature.layer.id === 'stops') { - const mapElement: MapElement = { + if ( + feature.layer.id === 'stops' || + feature.layer.id === 'stops-highlight' + ) { + const mapElement: MapStopElement = { isStop: true, name: feature.properties.stop_name, + locationType: Number(feature.properties.location_type), + stopId: feature.properties.stop_id, }; mapElements.push(mapElement); } else { @@ -138,6 +188,7 @@ export const GtfsVisualizationMap = ({ name: feature.properties.route_long_name, routeType: feature.properties.route_type, routeColor: feature.properties.route_color, + routeTextColor: feature.properties.route_text_color, routeId: feature.properties.route_id, }; mapElements.push(mapElement); @@ -200,6 +251,7 @@ export const GtfsVisualizationMap = ({ point[0], ]); + // TODO: example of how to use geojson in maplibre (stop density) const geojson: FeatureCollection = { type: 'FeatureCollection', features: [ @@ -214,6 +266,33 @@ export const GtfsVisualizationMap = ({ ], }; + const renderRouteTypeIcon = ( + routeTypeMetadata: RouteTypeMetadata, + routeColorText: string, + ): JSX.Element => { + const { icon: Icon } = routeTypeMetadata; + return ; + }; + + function getGradientBorder(colorString: string): string { + try { + const colors: string[] = JSON.parse(colorString); + + if (!Array.isArray(colors) || colors.length === 0) { + return 'none'; + } + + const gradient = `linear-gradient(to right, ${colors + .map((c) => `#${c}`) + .join(', ')})`; + + // Using border-image for gradient border + return `3px solid transparent; border-image: ${gradient} 1;`; + } catch { + return 'none'; + } + } + return ( @@ -222,7 +301,6 @@ export const GtfsVisualizationMap = ({ width: '100%', height: '100%', position: 'relative', - // border: '2px solid', borderColor: theme.palette.primary.main, borderRadius: '5px', }} @@ -238,7 +316,13 @@ export const GtfsVisualizationMap = ({ }} style={{ width: '100%', height: '100%' }} initialViewState={{ bounds }} - interactiveLayerIds={['stops', 'routes', 'routes-white']} + interactiveLayerIds={[ + 'stops', + 'routes', + 'routes-white', + 'routes-highlight', + 'stops-highlight', + ]} scrollZoom={true} dragPan={true} // https://pmtiles.io/ Good tool for debugging PMTiles @@ -256,13 +340,13 @@ export const GtfsVisualizationMap = ({ }, sample: { type: 'vector', - //url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/stops-v2.pmtiles', // Google Storage Bucket (CORS enabled) - url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/stops-bordeaux.pmtiles', // bordeaux + url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/stops-v2.pmtiles', // Google Storage Bucket (CORS enabled) + //url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/stops-bordeaux.pmtiles', // bordeaux }, routes: { type: 'vector', - //url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/routes-v2.pmtiles', // (STM) Google Storage Bucket (CORS enabled) - url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/routes-bordeaux.pmtiles', // bordeaux + url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/routes-v2.pmtiles', // (STM) Google Storage Bucket (CORS enabled) + //url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/routes-bordeaux.pmtiles', // bordeaux }, // boundingBox: { // type: 'geojson', @@ -318,12 +402,21 @@ export const GtfsVisualizationMap = ({ 4, // 1 = metro 3, // Default width ], - 'line-opacity': [ // Opacity based on whether the route is selected or not + 'line-opacity': [ + // Opacity based on whether the route is selected or not 'case', - ['any', ['==', filteredRoutes.length, 0], ['in', ['get', 'route_id'], ['literal', filteredRoutes]]], + [ + 'any', + ['==', filteredRoutes.length, 0], + [ + 'in', + ['get', 'route_id'], + ['literal', filteredRoutes], + ], + ], 0.4, // default opacity if selected or no filter 0.1, // faded if NOT in filteredRoutes - ] + ], }, // If routeTypesFilter includes a value -> show that // If routeTypesFilter is empty -> show all @@ -388,6 +481,11 @@ export const GtfsVisualizationMap = ({ 'any', ['in', ['get', 'route_id'], ['literal', hoverInfo]], ['in', ['get', 'route_id'], ['literal', filteredRoutes]], + [ + 'in', + ['get', 'route_id'], + ['literal', mapClickRouteData?.route_id ?? ''], + ], ], }, { @@ -396,28 +494,40 @@ export const GtfsVisualizationMap = ({ 'source-layer': 'stopsoutput', // Name of the geojson file when converting to pmtile. stops-output.geojson -> stopssoutput type: 'circle', paint: { - 'circle-radius': 6, - 'circle-color': generateStopColorExpression(routeIdToColorMap) as ExpressionSpecification, // VERY IMPORTANT: during the conversion to PMTiles, the route_colors are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" + 'circle-radius': 7, + 'circle-color': generateStopColorExpression( + routeIdToColorMap, + ) as ExpressionSpecification, // VERY IMPORTANT: during the conversion to PMTiles, the route_colors are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" 'circle-opacity': 0.8, }, minzoom: 10, maxzoom: 22, - filter: hideStops ? !hideStops : [ - 'any', - ['in', ['get', 'stop_id'], ['literal', hoverInfo]], - [ - 'any', - ...filteredRoutes.map((id) => { - return ['in', `\"${id}\"`, ['get', 'route_ids']] as any; // VERY IMPORTANT: during the conversion to PMTiles, the route_ids are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" - }), - ], - [ - 'any', - ...hoverInfo.map((id) => { - return ['in', `\"${id}\"`, ['get', 'route_ids']] as any; - }), - ], - ], + filter: hideStops + ? !hideStops + : [ + 'any', + ['in', ['get', 'stop_id'], ['literal', hoverInfo]], + [ + 'any', + ...filteredRoutes.map((id) => { + return [ + 'in', + `\"${id}\"`, + ['get', 'route_ids'], + ] as any; // VERY IMPORTANT: during the conversion to PMTiles, the route_ids are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" + }), + ], + [ + 'any', + ...hoverInfo.map((id) => { + return [ + 'in', + `\"${id}\"`, + ['get', 'route_ids'], + ] as any; + }), + ], + ], }, { id: 'stops-highlight-outer', @@ -429,23 +539,33 @@ export const GtfsVisualizationMap = ({ 'circle-color': '#ffffff', 'circle-opacity': 1, }, - filter: hideStops ? !hideStops : [ - 'any', - ['in', ['get', 'stop_id'], ['literal', hoverInfo]], - [ - 'any', - ...filteredRoutes.map((id) => { - return ['in', `\"${id}\"`, ['get', 'route_ids']] as any; // VERY IMPORTANT: during the conversion to PMTiles, the route_ids are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" - }), - ], - [ - 'any', - ...hoverInfo.map((id) => { - return ['in', `\"${id}\"`, ['get', 'route_ids']] as any; - }), - ], - ], - } + filter: hideStops + ? !hideStops + : [ + 'any', + ['in', ['get', 'stop_id'], ['literal', hoverInfo]], + [ + 'any', + ...filteredRoutes.map((id) => { + return [ + 'in', + `\"${id}\"`, + ['get', 'route_ids'], + ] as any; // VERY IMPORTANT: during the conversion to PMTiles, the route_ids are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" + }), + ], + [ + 'any', + ...hoverInfo.map((id) => { + return [ + 'in', + `\"${id}\"`, + ['get', 'route_ids'], + ] as any; + }), + ], + ], + }, ], }} > @@ -463,30 +583,11 @@ export const GtfsVisualizationMap = ({ 'rgba(0, 0, 0, 0.2) 0px 3px 5px -1px, rgba(0, 0, 0, 0.14) 0px 6px 10px 0px, rgba(0, 0, 0, 0.12) 0px 1px 18px 0px', }} /> - - {/*TODO: could be own component*/} - {Object.keys(mapClickData).length > 0 && ( - setMapClickData({})} - closeOnClick={false} - > -
- Clicked at: -
- {Number(mapClickData.latitude).toFixed(5)},{' '} - {Number(mapClickData.longitude).toFixed(5)} -
- {Object.entries(mapClickData).map(([key, value]) => ( -

- {key} - {value} -

- ))} -
-
- )} +
diff --git a/web-app/src/app/components/Map/MapDataPopup.tsx b/web-app/src/app/components/Map/MapDataPopup.tsx new file mode 100644 index 000000000..05ea0c85f --- /dev/null +++ b/web-app/src/app/components/Map/MapDataPopup.tsx @@ -0,0 +1,169 @@ +import { Popup } from 'react-map-gl/maplibre'; +import { + locationTypesMapping, + RouteTypeMetadata, + routeTypesMapping, +} from '../../constants/RouteTypes'; +import { Box, Link, Typography, useTheme } from '@mui/material'; +import AccessibleIcon from '@mui/icons-material/Accessible'; + +interface MapDataPopupProps { + mapClickRouteData: Record | null; + mapClickStopData: Record | null; + onPopupClose: () => void; +} + +export const MapDataPopup = ( + props: React.PropsWithChildren, +): JSX.Element => { + const { mapClickRouteData, mapClickStopData, onPopupClose } = props; + const theme = useTheme(); + + const renderRouteTypeIcon = ( + routeTypeMetadata: RouteTypeMetadata, + routeColorText: string, + ): JSX.Element => { + const { icon: Icon } = routeTypeMetadata; + return ; + }; + + function getGradientBorder(colorString: string): string { + try { + const colors: string[] = JSON.parse(colorString); + + if (!Array.isArray(colors) || colors.length === 0) { + return 'none'; + } + + const gradient = `linear-gradient(to right, ${colors + .map((c) => `#${c}`) + .join(', ')})`; + + // Using border-image for gradient border + return `6px solid transparent; border-image: ${gradient} 1;`; + } catch { + return 'none'; + } + } + + return ( + <> + {mapClickRouteData != null && ( + + + + {renderRouteTypeIcon( + routeTypesMapping[mapClickRouteData.route_type], + mapClickRouteData.route_text_color, + )} + + {routeTypesMapping[mapClickRouteData.route_type].name}{' '} + + {mapClickRouteData.route_id} + + + + + + {mapClickRouteData.route_long_name} + + +

{mapClickRouteData.agency_name}

+
+
+ )} + {mapClickStopData != null && ( + + + + + {renderRouteTypeIcon( + locationTypesMapping[mapClickStopData.location_type], + '000000', + )} + + {locationTypesMapping[mapClickStopData.location_type].name}{' '} + + {mapClickStopData.stop_id} + + + + {mapClickStopData.wheelchair_boarding === '1' && ( + + )} + + + + {mapClickStopData.stop_name} + + + {mapClickStopData.route_ids != null && + mapClickStopData.route_ids.replace(/[\[\]"\\]/g, ',').length > + 0 && ( + + Route Ids + + {mapClickStopData.route_ids.replace(/[\[\]"\\]/g, ' ')} + + + )} + + {mapClickStopData.stop_code != null && ( + + Stop Code + {mapClickStopData.stop_code} + + )} + {mapClickStopData.stop_url != null && ( + + View Stop Info + + )} + + + + )} + + ); +}; diff --git a/web-app/src/app/components/MapElement.tsx b/web-app/src/app/components/MapElement.tsx index dbe62c2c6..df7a54d45 100644 --- a/web-app/src/app/components/MapElement.tsx +++ b/web-app/src/app/components/MapElement.tsx @@ -1,17 +1,31 @@ import * as React from 'react'; import 'leaflet/dist/leaflet.css'; -import { Box, Typography } from '@mui/material'; -import SubwayIcon from '@mui/icons-material/Subway'; -import DirectionsBusIcon from '@mui/icons-material/DirectionsBus'; +import { Box, Typography, useTheme } from '@mui/material'; +import { + locationTypesMapping, + RouteTypeMetadata, + routeTypesMapping, +} from '../constants/RouteTypes'; -export interface MapElement { +export interface BaseMapElement { isStop: boolean; - routeType?: number; name: string; - routeColor?: string; - routeId?: string; } +export interface MapRouteElement extends BaseMapElement { + routeType: number; + routeColor: string; + routeTextColor: string; + routeId: string; +} + +export interface MapStopElement extends BaseMapElement { + locationType: number; + stopId: string; +} + +export type MapElement = MapRouteElement | MapStopElement; + export interface MapElementProps { mapElements: MapElement[]; } @@ -19,27 +33,77 @@ export interface MapElementProps { export const MapElement = ( props: React.PropsWithChildren, ): JSX.Element => { + const theme = useTheme(); + const formatSet = new Set(); + const formattedElements: MapElement[] = []; - const formatSet = new Set(); - const formattedElements: MapElement[] = []; - // fE2 is not being used - to be completed - const fE2: {[key: string]: MapElement[]}= { - ['stops']: [], - ['routes']: [] + props.mapElements.forEach((element) => { + if (!formatSet.has(element.name)) { + formattedElements.push(element); } - props.mapElements.forEach((element) => { - if(!formatSet.has(element.name)) { - formattedElements.push(element) - if(element.isStop) { - fE2['stops'].push(element) - } else { - fE2['routes'].push(element) - } - } - formatSet.add(element.name) - }); + formatSet.add(element.name); + }); + + // TODO: duplicate + const renderRouteTypeIcon = ( + routeTypeMetadata: RouteTypeMetadata, + routeColorText: string, + ): JSX.Element => { + const { icon: Icon } = routeTypeMetadata; + return ; + }; + + const renderRouteMapElement = (element: MapRouteElement): JSX.Element => { + return ( + + {/* Render the route type icon */} + {renderRouteTypeIcon( + routeTypesMapping[element.routeType?.toString() || '0'], + element.routeTextColor || '000000', + )} + + + {element.routeId} - {element.name} + + + ); + }; + + const renderStopMapElement = (element: MapStopElement): JSX.Element => { + return ( + + {/* Render the route type icon */} + {renderRouteTypeIcon( + locationTypesMapping[element.locationType?.toString() || '0'], + '000000', + )} - + + {element.stopId} - {element.name} + + + ); + }; return ( { return ( - {element.isStop ? "Stop" :"Route"} + + {element.isStop ? 'Stop' : 'Route'} + {element.isStop ? ( - - {element.name} - + <>{renderStopMapElement(element as MapStopElement)} ) : ( - - {element.routeType == 1 ? ( - - ) : ( - - )} - - {element.routeId} - {element.name} - - + <>{renderRouteMapElement(element as MapRouteElement)} )} ); })} ); -}; \ No newline at end of file +}; diff --git a/web-app/src/app/constants/RouteTypes.ts b/web-app/src/app/constants/RouteTypes.ts deleted file mode 100644 index 1748659bb..000000000 --- a/web-app/src/app/constants/RouteTypes.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const routeTypesMapping: Record = { - '0': 'Tram', - '1': 'Subway', - '2': 'Rail', - '3': 'Bus', - '4': 'Ferry', - '5': 'Cable Car', - '6': 'Gondola', - '7': 'Funicular', - '11': 'Monorail', - '12': 'High Speed Train', - '13': 'Airplane', -}; - -export const reversedRouteTypesMapping = Object.fromEntries( - Object.entries(routeTypesMapping).map(([k, v]) => [v, k]), -); diff --git a/web-app/src/app/constants/RouteTypes.tsx b/web-app/src/app/constants/RouteTypes.tsx new file mode 100644 index 000000000..d6afbb0da --- /dev/null +++ b/web-app/src/app/constants/RouteTypes.tsx @@ -0,0 +1,48 @@ +import TramIcon from '@mui/icons-material/Tram'; +import SubwayIcon from '@mui/icons-material/Subway'; +import TrainIcon from '@mui/icons-material/Train'; +import DirectionsBusIcon from '@mui/icons-material/DirectionsBus'; +import DirectionsBoatIcon from '@mui/icons-material/DirectionsBoat'; +import CableIcon from '@mui/icons-material/Cable'; +import EmojiTransportationIcon from '@mui/icons-material/EmojiTransportation'; // for Gondola +import TerrainIcon from '@mui/icons-material/Terrain'; // for Funicular +import DepartureBoardIcon from '@mui/icons-material/DepartureBoard'; // for Monorail +import SpeedIcon from '@mui/icons-material/Speed'; // for High Speed Train +import FlightIcon from '@mui/icons-material/Flight'; +import MeetingRoomIcon from '@mui/icons-material/MeetingRoom'; +import PlaceIcon from '@mui/icons-material/Place'; +import DirectionsSubwayIcon from '@mui/icons-material/DirectionsSubway'; +import type { SvgIconComponent } from '@mui/icons-material'; + +export interface RouteTypeMetadata { + name: string; + icon: SvgIconComponent; +} + +export type LocationTypeMetadata = RouteTypeMetadata; + +export const routeTypesMapping: Record = { + '0': { name: 'Tram', icon: TramIcon }, + '1': { name: 'Subway', icon: SubwayIcon }, + '2': { name: 'Rail', icon: TrainIcon }, + '3': { name: 'Bus', icon: DirectionsBusIcon }, + '4': { name: 'Ferry', icon: DirectionsBoatIcon }, + '5': { name: 'Cable Car', icon: CableIcon }, + '6': { name: 'Gondola', icon: EmojiTransportationIcon }, + '7': { name: 'Funicular', icon: TerrainIcon }, + '11': { name: 'Monorail', icon: DepartureBoardIcon }, + '12': { name: 'High Speed Train', icon: SpeedIcon }, + '13': { name: 'Airplane', icon: FlightIcon }, +}; + +// TODO: find better icons for these +export const locationTypesMapping: Record = { + '0': { name: 'Stop', icon: DirectionsBusIcon }, + '1': { name: 'Station', icon: TrainIcon }, + '2': { name: 'Entrance/Exit', icon: MeetingRoomIcon }, + '3': { name: 'Generic Node', icon: PlaceIcon }, + '4': { name: 'Boarding Area', icon: DirectionsSubwayIcon }, +}; +export const reversedRouteTypesMapping = Object.fromEntries( + Object.entries(routeTypesMapping).map(([k, v]) => [v.name, k]), +); diff --git a/web-app/src/app/screens/Feed/Map.styles.ts b/web-app/src/app/screens/Feed/Map.styles.ts index 50db9b01c..8e14570de 100644 --- a/web-app/src/app/screens/Feed/Map.styles.ts +++ b/web-app/src/app/screens/Feed/Map.styles.ts @@ -1,4 +1,4 @@ -import { Box, styled} from "@mui/material"; +import { Box, styled } from '@mui/material'; interface StyledMapControlPanelProps { showMapControlMobile: boolean; @@ -11,7 +11,7 @@ export const StyledMapControlPanel = styled(Box, { paddingTop: '100px', // to account for the fixed header on mobile flexDirection: 'column', flexWrap: 'nowrap', - backgroundColor: theme.palette.background.default, + backgroundColor: theme.palette.background.paper, zIndex: 10000, display: showMapControlMobile ? 'flex' : 'none', width: '100%', @@ -19,13 +19,16 @@ export const StyledMapControlPanel = styled(Box, { top: 0, height: '100%', overflowY: 'auto', + margin: 0, + borderRadius: theme.shape.borderRadius, [theme.breakpoints.up('md')]: { display: 'flex', width: '300px', position: 'relative', top: 'unset', - paddingTop: 0 + paddingTop: 0, + margin: theme.spacing(2), }, })); @@ -43,4 +46,3 @@ export const StyledChipFilterContainer = styled(Box)(({ theme }) => ({ overflowX: 'hidden', }, })); - diff --git a/web-app/src/app/screens/Feed/components/FullMapView.tsx b/web-app/src/app/screens/Feed/components/FullMapView.tsx index 903fd4295..b380e95d9 100644 --- a/web-app/src/app/screens/Feed/components/FullMapView.tsx +++ b/web-app/src/app/screens/Feed/components/FullMapView.tsx @@ -1,7 +1,7 @@ import { Box, Fab, Button, Chip, useTheme } from '@mui/material'; import RouteSelector from '../../../components/RouteSelector'; -//import sampleRoutes from './sample-route-output.json'; // STM -import sampleRoutes from './routes_TBM-2622_sample.json'; // BOrdaux +import sampleRoutes from './sample-route-output.json'; // STM +//import sampleRoutes from './routes_TBM-2622_sample.json'; // BOrdaux import React, { useState } from 'react'; import { GtfsVisualizationMap } from '../../../components/GtfsVisualizationMap'; import CloseIcon from '@mui/icons-material/Close'; @@ -48,8 +48,8 @@ export default function FullMapView(): React.ReactElement { } }); return Array.from(uniqueTypes).map((type) => ({ - title: routeTypesMapping[type], - checked: filteredRouteTypes.includes(routeTypesMapping[type]), + title: routeTypesMapping[type].name, + checked: filteredRouteTypes.includes(routeTypesMapping[type].name), props: { routeTypeId: type, }, @@ -58,12 +58,12 @@ export default function FullMapView(): React.ReactElement { }; // // TODO: this is hardcoded for Montreal, should be dynamic - // const bb = [ - // [45.402668, -73.956204], - // [45.402668, -73.480581], - // [45.701116, -73.480581], - // [45.701116, -73.956204], - // ]; + const bb = [ + [45.402668, -73.956204], + [45.402668, -73.480581], + [45.701116, -73.480581], + [45.701116, -73.956204], + ]; // const bb = [ // Big France // [ @@ -84,14 +84,14 @@ export default function FullMapView(): React.ReactElement { // ] // ] - const bb = [ - // bordeaux + // const bb = [ + // // bordeaux - [44.754866, -0.79898], - [44.754866, -0.464777], - [45.025554, -0.464777], - [45.025554, -0.79898], - ]; + // [44.754866, -0.79898], + // [44.754866, -0.464777], + // [45.025554, -0.464777], + // [45.025554, -0.79898], + // ]; const renderFilterChips = (): React.ReactElement => { return ( @@ -170,7 +170,7 @@ export default function FullMapView(): React.ReactElement { Date: Fri, 27 Jun 2025 11:58:39 -0400 Subject: [PATCH 14/16] bounding box state + dark mode support --- web-app/src/app/Theme.ts | 16 ++++ .../app/components/GtfsVisualizationMap.tsx | 44 +++------- .../src/app/components/Map/MapDataPopup.tsx | 24 ++++-- web-app/src/app/components/MapElement.tsx | 12 ++- .../screens/Feed/components/FullMapView.tsx | 81 +++++++++---------- 5 files changed, 90 insertions(+), 87 deletions(-) diff --git a/web-app/src/app/Theme.ts b/web-app/src/app/Theme.ts index debe32a49..2b095587a 100644 --- a/web-app/src/app/Theme.ts +++ b/web-app/src/app/Theme.ts @@ -12,6 +12,17 @@ declare module '@mui/material/styles' { interface PaletteOptions { boxShadow?: string; } + interface Theme { + map: { + basemapTileUrl: string; + }; + } + + interface ThemeOptions { + map?: { + basemapTileUrl?: string; + }; + } } declare module '@mui/material/Typography' { @@ -96,6 +107,11 @@ export const getTheme = (mode: ThemeModeEnum): Theme => { const chosenPalette = !isLightMode ? darkPalette : palette; return createTheme({ palette: { ...chosenPalette, mode }, + map: { + basemapTileUrl: isLightMode + ? 'https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' + : 'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', + }, mixins: { code: { contrastText: '#f1fa8c', diff --git a/web-app/src/app/components/GtfsVisualizationMap.tsx b/web-app/src/app/components/GtfsVisualizationMap.tsx index 5044be5b2..d990aa029 100644 --- a/web-app/src/app/components/GtfsVisualizationMap.tsx +++ b/web-app/src/app/components/GtfsVisualizationMap.tsx @@ -19,9 +19,9 @@ import { Box, useTheme } from '@mui/material'; import { MapElement, MapRouteElement, MapStopElement } from './MapElement'; import { reversedRouteTypesMapping, - RouteTypeMetadata, } from '../constants/RouteTypes'; import { MapDataPopup } from './Map/MapDataPopup'; +import { useTheme as themeProvider } from '../context/ThemeProvider'; export interface GtfsVisualizationMapProps { polygon: LatLngExpression[]; @@ -266,33 +266,6 @@ export const GtfsVisualizationMap = ({ ], }; - const renderRouteTypeIcon = ( - routeTypeMetadata: RouteTypeMetadata, - routeColorText: string, - ): JSX.Element => { - const { icon: Icon } = routeTypeMetadata; - return ; - }; - - function getGradientBorder(colorString: string): string { - try { - const colors: string[] = JSON.parse(colorString); - - if (!Array.isArray(colors) || colors.length === 0) { - return 'none'; - } - - const gradient = `linear-gradient(to right, ${colors - .map((c) => `#${c}`) - .join(', ')})`; - - // Using border-image for gradient border - return `3px solid transparent; border-image: ${gradient} 1;`; - } catch { - return 'none'; - } - } - return ( @@ -331,9 +304,7 @@ export const GtfsVisualizationMap = ({ sources: { 'raster-tiles': { type: 'raster', - tiles: [ - 'https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', - ], + tiles: [theme.map.basemapTileUrl], tileSize: 256, attribution: '© OpenStreetMap contributors', @@ -371,7 +342,7 @@ export const GtfsVisualizationMap = ({ type: 'line', paint: { // style and color of the stops - 'line-color': '#ffffff', + 'line-color': theme.palette.background.paper, 'line-width': [ 'match', ['get', 'route_type'], // Property from the vector tile @@ -498,7 +469,7 @@ export const GtfsVisualizationMap = ({ 'circle-color': generateStopColorExpression( routeIdToColorMap, ) as ExpressionSpecification, // VERY IMPORTANT: during the conversion to PMTiles, the route_colors are stored as strings with quotes NOT arrays. [1,2,3] -> "["1","2","3"]" - 'circle-opacity': 0.8, + 'circle-opacity': 1, }, minzoom: 10, maxzoom: 22, @@ -507,6 +478,11 @@ export const GtfsVisualizationMap = ({ : [ 'any', ['in', ['get', 'stop_id'], ['literal', hoverInfo]], + [ + 'in', + ['get', 'stop_id'], + ['literal', mapClickStopData?.stop_id ?? ''], + ], [ 'any', ...filteredRoutes.map((id) => { @@ -536,7 +512,7 @@ export const GtfsVisualizationMap = ({ type: 'circle', paint: { 'circle-radius': 3, - 'circle-color': '#ffffff', + 'circle-color': theme.palette.background.paper, 'circle-opacity': 1, }, filter: hideStops diff --git a/web-app/src/app/components/Map/MapDataPopup.tsx b/web-app/src/app/components/Map/MapDataPopup.tsx index 05ea0c85f..29df837b4 100644 --- a/web-app/src/app/components/Map/MapDataPopup.tsx +++ b/web-app/src/app/components/Map/MapDataPopup.tsx @@ -56,7 +56,7 @@ export const MapDataPopup = ( onClose={onPopupClose} closeOnClick={true} style={{ - minWidth: '200px' + minWidth: '200px', }} > {renderRouteTypeIcon( locationTypesMapping[mapClickStopData.location_type], - '000000', + theme.palette.text.primary, )} {locationTypesMapping[mapClickStopData.location_type].name}{' '} @@ -123,11 +125,23 @@ export const MapDataPopup = ( {mapClickStopData.wheelchair_boarding === '1' && ( - + )} - + {mapClickStopData.stop_name} @@ -155,7 +169,7 @@ export const MapDataPopup = ( target='_blank' rel='noreferrer' variant={'body2'} - sx={{mt:1}} + sx={{ mt: 1 }} > View Stop Info diff --git a/web-app/src/app/components/MapElement.tsx b/web-app/src/app/components/MapElement.tsx index df7a54d45..60d92046e 100644 --- a/web-app/src/app/components/MapElement.tsx +++ b/web-app/src/app/components/MapElement.tsx @@ -50,7 +50,7 @@ export const MapElement = ( routeColorText: string, ): JSX.Element => { const { icon: Icon } = routeTypeMetadata; - return ; + return ; }; const renderRouteMapElement = (element: MapRouteElement): JSX.Element => { @@ -68,10 +68,9 @@ export const MapElement = ( borderRadius: '5px', }} > - {/* Render the route type icon */} {renderRouteTypeIcon( routeTypesMapping[element.routeType?.toString() || '0'], - element.routeTextColor || '000000', + element.routeTextColor ? ('#' + element.routeTextColor) : '#000000', )} @@ -81,7 +80,7 @@ export const MapElement = ( ); }; - const renderStopMapElement = (element: MapStopElement): JSX.Element => { + const renderStopMapElement = (element: MapStopElement, iconColor: string): JSX.Element => { return ( - {/* Render the route type icon */} {renderRouteTypeIcon( locationTypesMapping[element.locationType?.toString() || '0'], - '000000', + iconColor, )} @@ -132,7 +130,7 @@ export const MapElement = ( {element.isStop ? 'Stop' : 'Route'} {element.isStop ? ( - <>{renderStopMapElement(element as MapStopElement)} + <>{renderStopMapElement(element as MapStopElement, theme.palette.text.primary)} ) : ( <>{renderRouteMapElement(element as MapRouteElement)} )} diff --git a/web-app/src/app/screens/Feed/components/FullMapView.tsx b/web-app/src/app/screens/Feed/components/FullMapView.tsx index b380e95d9..236172411 100644 --- a/web-app/src/app/screens/Feed/components/FullMapView.tsx +++ b/web-app/src/app/screens/Feed/components/FullMapView.tsx @@ -17,15 +17,29 @@ import { StyledChipFilterContainer, StyledMapControlPanel, } from '../Map.styles'; +import { + selectBoundingBoxFromLatestDataset, + selectDatasetsLoadingStatus, + selectLatestDatasetsData, +} from '../../../store/selectors'; +import { useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { loadingDataset } from '../../../store/dataset-reducer'; +import { useAppDispatch } from '../../../hooks'; export default function FullMapView(): React.ReactElement { const { t } = useTranslation('feeds'); + const { feedId, feedDataType } = useParams(); const theme = useTheme(); const [filteredRoutes, setFilteredRoutes] = useState([]); const [filteredRouteTypes, setFilteredRouteTypes] = useState([]); const [hideStops, setHideStops] = useState(false); const [showMapControlMobile, setShowMapControlMobile] = useState(false); + const latestDataset = useSelector(selectLatestDatasetsData); + const boundingBox = useSelector(selectBoundingBoxFromLatestDataset); + const datasetLoadingStatus = useSelector(selectDatasetsLoadingStatus); + const dispatch = useAppDispatch(); const clearAllFilters = (): void => { setFilteredRoutes([]); @@ -57,41 +71,24 @@ export default function FullMapView(): React.ReactElement { })) as CheckboxStructure[]; }; - // // TODO: this is hardcoded for Montreal, should be dynamic - const bb = [ - [45.402668, -73.956204], - [45.402668, -73.480581], - [45.701116, -73.480581], - [45.701116, -73.956204], - ]; - - // const bb = [ // Big France - // [ - // 42.751541, - // -1.79019 - // ], - // [ - // 42.751541, - // 7.734793 - // ], - // [ - // 50.6394, - // 7.734793 - // ], - // [ - // 50.6394, - // -1.79019 - // ] - // ] - - // const bb = [ - // // bordeaux - - // [44.754866, -0.79898], - // [44.754866, -0.464777], - // [45.025554, -0.464777], - // [45.025554, -0.79898], - // ]; + // since this is a page on it's own, we can check if the bounding box is set in state, if not we have no choice but to fetch it + // console.log( + // 'boundingBox from state', + // boundingBox, + // latestDataset, + // datasetLoadingStatus, + // feedId, + // ); + if (boundingBox == undefined && latestDataset == undefined) { + if (feedId != undefined) { + dispatch( + loadingDataset({ + feedId, + limit: 1, + }), + ); + } + } const renderFilterChips = (): React.ReactElement => { return ( @@ -307,12 +304,14 @@ export default function FullMapView(): React.ReactElement { > - + {boundingBox != undefined && ( + + )} From 06daac951b3e999a73060a74ed5e122d9273c53c Mon Sep 17 00:00:00 2001 From: jcpitre Date: Mon, 14 Jul 2025 09:29:31 -0400 Subject: [PATCH 15/16] Removed an error when the route type is not in the specs. --- web-app/src/app/components/MapElement.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web-app/src/app/components/MapElement.tsx b/web-app/src/app/components/MapElement.tsx index 60d92046e..ac0c6a5ee 100644 --- a/web-app/src/app/components/MapElement.tsx +++ b/web-app/src/app/components/MapElement.tsx @@ -48,7 +48,12 @@ export const MapElement = ( const renderRouteTypeIcon = ( routeTypeMetadata: RouteTypeMetadata, routeColorText: string, - ): JSX.Element => { + ): JSX.Element | null => { + // The route type could be out of specs (e.g. google route types), so we may not have an icon. + if (!routeTypeMetadata || !routeTypeMetadata.icon) { + // Optionally render a default icon or return null + return null; + } const { icon: Icon } = routeTypeMetadata; return ; }; From 2b0ef1804e8600c972c479f257896bd171bfb1e0 Mon Sep 17 00:00:00 2001 From: jcpitre Date: Tue, 19 Aug 2025 09:13:14 -0400 Subject: [PATCH 16/16] Modified to select the right pmtiles --- .../tasks/pmtiles_builder/build_pmtiles.py | 15 ++++++ web-app/src/app/components/CoveredAreaMap.tsx | 5 +- .../app/components/GtfsVisualizationMap.tsx | 48 +++++++++++++++++-- .../src/app/components/Map/MapDataPopup.tsx | 2 +- web-app/src/app/components/MapElement.tsx | 4 +- web-app/src/app/components/RouteSelector.tsx | 4 +- web-app/src/app/screens/Feed/Feed.styles.ts | 2 +- .../screens/Feed/components/FullMapView.tsx | 3 +- web-app/src/app/screens/Feeds/Feeds.styles.ts | 2 +- web-app/src/app/styles/Filters.styles.ts | 4 +- 10 files changed, 75 insertions(+), 14 deletions(-) diff --git a/functions-python/tasks_executor/src/tasks/pmtiles_builder/build_pmtiles.py b/functions-python/tasks_executor/src/tasks/pmtiles_builder/build_pmtiles.py index d899b628d..6c0b5136a 100644 --- a/functions-python/tasks_executor/src/tasks/pmtiles_builder/build_pmtiles.py +++ b/functions-python/tasks_executor/src/tasks/pmtiles_builder/build_pmtiles.py @@ -256,6 +256,21 @@ def _upload_files_to_gcs(self, file_to_upload): self.bucket_name, blob_path, ) + + try: + blob.make_public() + self.logger.debug( + "Made object public: https://storage.googleapis.com/%s/%s", + self.bucket_name, + blob_path, + ) + except Exception as e: + # Likely due to Uniform bucket-level access; log and continue + self.logger.warning( + "Could not make %s public (uniform bucket-level access enabled?): %s", + blob_path, + e, + ) except Exception as e: raise Exception(f"Failed to upload files to GCS: {e}") from e diff --git a/web-app/src/app/components/CoveredAreaMap.tsx b/web-app/src/app/components/CoveredAreaMap.tsx index 63340d109..d94db19aa 100644 --- a/web-app/src/app/components/CoveredAreaMap.tsx +++ b/web-app/src/app/components/CoveredAreaMap.tsx @@ -166,7 +166,10 @@ const CoveredAreaMap: React.FC = ({ > - + ); } diff --git a/web-app/src/app/components/GtfsVisualizationMap.tsx b/web-app/src/app/components/GtfsVisualizationMap.tsx index d990aa029..c45054f07 100644 --- a/web-app/src/app/components/GtfsVisualizationMap.tsx +++ b/web-app/src/app/components/GtfsVisualizationMap.tsx @@ -1,6 +1,6 @@ /* eslint-disable */ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, useMemo } from 'react'; import Map, { type MapRef, MapProvider, @@ -23,8 +23,15 @@ import { import { MapDataPopup } from './Map/MapDataPopup'; import { useTheme as themeProvider } from '../context/ThemeProvider'; +interface LatestDatasetLite { + hosted_url?: string; + id?: string; + stable_id?: string; +} + export interface GtfsVisualizationMapProps { polygon: LatLngExpression[]; + latestDataset?: LatestDatasetLite; filteredRoutes?: string[]; filteredRouteTypes?: string[]; hideStops?: boolean; @@ -32,10 +39,45 @@ export interface GtfsVisualizationMapProps { export const GtfsVisualizationMap = ({ polygon, + latestDataset, filteredRoutes = [], filteredRouteTypes = [], hideStops = false, }: GtfsVisualizationMapProps): JSX.Element => { + + console.log('[GtfsVisualizationMap] mount', { + latestDatasetId: latestDataset?.id, + latestDatasetHostedUrl: latestDataset?.hosted_url, + }); + + const { stopsPmtilesUrl, routesPmtilesUrl } = useMemo(() => { + const baseUrl = latestDataset?.hosted_url ? latestDataset.hosted_url.replace(/[^/]+$/, '') : undefined; + console.log('[GtfsVisualizationMap] latestDataset URLs', { + hostedUrl: latestDataset?.hosted_url, + baseUrl, + }); + + const stops = `${baseUrl}/pmtiles/stops.pmtiles`; + const routes = `${baseUrl}/pmtiles/routes.pmtiles`; + + return { stopsPmtilesUrl: stops, routesPmtilesUrl: routes }; + }, [latestDataset?.id, latestDataset?.stable_id]); + + + useEffect(() => { + console.log('[GtfsVisualizationMap] PMTiles URLs', { + stopsPmtilesUrl, + routesPmtilesUrl, + }); + }, [stopsPmtilesUrl, routesPmtilesUrl]); + + // Log whenever the identifiers change + useEffect(() => { + console.log('[GtfsVisualizationMap] props update', { + latestDatasetId: latestDataset?.id, + }); + }, [latestDataset?.id]); + const theme = useTheme(); const [hoverInfo, setHoverInfo] = useState([]); const [hoverData, setHoverData] = useState(''); @@ -311,12 +353,12 @@ export const GtfsVisualizationMap = ({ }, sample: { type: 'vector', - url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/stops-v2.pmtiles', // Google Storage Bucket (CORS enabled) + url: `pmtiles://${stopsPmtilesUrl}`, // dynamic stops //url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/stops-bordeaux.pmtiles', // bordeaux }, routes: { type: 'vector', - url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/routes-v2.pmtiles', // (STM) Google Storage Bucket (CORS enabled) + url: `pmtiles://${routesPmtilesUrl}`, // dynamic routes //url: 'pmtiles://https://storage.googleapis.com/map-details-bucket-test/routes-bordeaux.pmtiles', // bordeaux }, // boundingBox: { diff --git a/web-app/src/app/components/Map/MapDataPopup.tsx b/web-app/src/app/components/Map/MapDataPopup.tsx index 29df837b4..f732815ce 100644 --- a/web-app/src/app/components/Map/MapDataPopup.tsx +++ b/web-app/src/app/components/Map/MapDataPopup.tsx @@ -1,7 +1,7 @@ import { Popup } from 'react-map-gl/maplibre'; import { locationTypesMapping, - RouteTypeMetadata, + type RouteTypeMetadata, routeTypesMapping, } from '../../constants/RouteTypes'; import { Box, Link, Typography, useTheme } from '@mui/material'; diff --git a/web-app/src/app/components/MapElement.tsx b/web-app/src/app/components/MapElement.tsx index ac0c6a5ee..4ddbe1185 100644 --- a/web-app/src/app/components/MapElement.tsx +++ b/web-app/src/app/components/MapElement.tsx @@ -3,7 +3,7 @@ import 'leaflet/dist/leaflet.css'; import { Box, Typography, useTheme } from '@mui/material'; import { locationTypesMapping, - RouteTypeMetadata, + type RouteTypeMetadata, routeTypesMapping, } from '../constants/RouteTypes'; @@ -75,7 +75,7 @@ export const MapElement = ( > {renderRouteTypeIcon( routeTypesMapping[element.routeType?.toString() || '0'], - element.routeTextColor ? ('#' + element.routeTextColor) : '#000000', + element.routeTextColor ? '#' + element.routeTextColor : '#000000', )} diff --git a/web-app/src/app/components/RouteSelector.tsx b/web-app/src/app/components/RouteSelector.tsx index c79412f7f..12a63e35a 100644 --- a/web-app/src/app/components/RouteSelector.tsx +++ b/web-app/src/app/components/RouteSelector.tsx @@ -11,7 +11,7 @@ import { } from '@mui/material'; interface RouteSelectorProps { - routes: Array; // Replace 'any' with a more specific type if available + routes: any[]; // Replace 'any' with a more specific type if available selectedRouteIds?: string[]; onSelectionChange?: (selectedRoutes: string[]) => void; } @@ -72,7 +72,7 @@ export default function RouteSelector({ onChange={(e) => setSearch(e.target.value)} /> - + {filteredRoutes .sort((a, b) => a.routeId - b.routeId) .map((route) => ( diff --git a/web-app/src/app/screens/Feed/Feed.styles.ts b/web-app/src/app/screens/Feed/Feed.styles.ts index 6c70f6be3..1fedd1833 100644 --- a/web-app/src/app/screens/Feed/Feed.styles.ts +++ b/web-app/src/app/screens/Feed/Feed.styles.ts @@ -74,7 +74,7 @@ export const StyledListItem = styled('li')(({ theme }) => ({ margin: '5px 0', fontWeight: 'normal', fontSize: '16px', -})) +})); export const ResponsiveListItem = styled(StyledListItem)(({ theme }) => ({ [theme.breakpoints.up('lg')]: { diff --git a/web-app/src/app/screens/Feed/components/FullMapView.tsx b/web-app/src/app/screens/Feed/components/FullMapView.tsx index 236172411..88d2d2c2b 100644 --- a/web-app/src/app/screens/Feed/components/FullMapView.tsx +++ b/web-app/src/app/screens/Feed/components/FullMapView.tsx @@ -1,7 +1,7 @@ import { Box, Fab, Button, Chip, useTheme } from '@mui/material'; import RouteSelector from '../../../components/RouteSelector'; import sampleRoutes from './sample-route-output.json'; // STM -//import sampleRoutes from './routes_TBM-2622_sample.json'; // BOrdaux +// import sampleRoutes from './routes_TBM-2622_sample.json'; // BOrdaux import React, { useState } from 'react'; import { GtfsVisualizationMap } from '../../../components/GtfsVisualizationMap'; import CloseIcon from '@mui/icons-material/Close'; @@ -307,6 +307,7 @@ export default function FullMapView(): React.ReactElement { {boundingBox != undefined && ( = (theme) => ({ display: 'flex', diff --git a/web-app/src/app/styles/Filters.styles.ts b/web-app/src/app/styles/Filters.styles.ts index 54840913b..c0f180cdc 100644 --- a/web-app/src/app/styles/Filters.styles.ts +++ b/web-app/src/app/styles/Filters.styles.ts @@ -1,4 +1,4 @@ -import { styled, Typography } from "@mui/material"; +import { styled, Typography } from '@mui/material'; export const SearchHeader = styled(Typography)(({ theme }) => ({ '&.no-collapse': { @@ -14,4 +14,4 @@ export const SearchHeader = styled(Typography)(({ theme }) => ({ width: '104px', background: theme.palette.text.primary, }, -})); \ No newline at end of file +}));