Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,14 @@ import 'react-widgets/dist/css/react-widgets.css';
import 'react-toggle/style.css';

import React, { Component } from 'react';
import {
QueryRenderer,
graphql,
} from 'react-relay';
import { QueryRenderer, graphql } from 'react-relay';
import Moment from 'moment/moment';
import momentLocalizer from 'react-widgets-moment';
import Map from './components/Map';
import './styles/App.css';
import './styles/Zoom.css';
import environment from './relayEnv';

// needed to render DateTime component
// https://jquense.github.io/react-widgets/localization/
Moment.locale('en');
momentLocalizer();

Expand All @@ -24,9 +19,14 @@ class App extends Component {
super();
this.state = {
environment,
agency: 'muni', // ✅ default agency in state
};
}

handleAgencyChange = (selectedAgency) => {
this.setState({ agency: selectedAgency }); // ✅ triggers re-query
}

loadRelay() {
return (
<QueryRenderer
Expand All @@ -37,16 +37,24 @@ class App extends Component {
}
`}
variables={{
agency: 'muni',
agency: this.state.agency, // ✅ dynamic, not hardcoded
startTime: Date.now() - 15000,
endTime: Date.now(),
}}
render={({ error, props }) => {
console.log(props);
if (error) {
return <div>{error.message}</div>;
}
return (<div>{props && <Map trynState={props} />}</div>);
return (
<div>
{props && (
<Map
trynState={props}
onAgencyChange={this.handleAgencyChange}
/>
)}
</div>
);
}}
/>
);
Expand All @@ -61,4 +69,4 @@ class App extends Component {
}
}

export default App;
export default App;
9 changes: 7 additions & 2 deletions src/components/ControlPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,12 @@ class ControlPanel extends Component {
sortedRoutes: this.getCityRoutes(city.value),
}, () => {
this.props
.setMapLocation(city.value.latitude, city.value.longitude, 11);
.setMapLocation(
city.value.latitude,
city.value.longitude,
11,
city.value.name, // ✅ FIXED: pass city name so Map.jsx can resolve correct agencyId
);
this.props.clearSelectedRoutes();
this.refetch();
})
Expand Down Expand Up @@ -99,4 +104,4 @@ ControlPanel.propTypes = {
clearSelectedRoutes: propTypes.func.isRequired,
};

export default ControlPanel;
export default ControlPanel;
65 changes: 35 additions & 30 deletions src/components/Map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ import {
import ControlPanel from './ControlPanel';
import Stop from './Stop';

// ✅ Maps city names to agency IDs used in GraphQL API
const agencyMap = {
'San Francisco': 'muni',
'Toronto': 'ttc',
};

class Map extends Component {
constructor() {
super();
this.state = {
// Viewport settings that is shared between mapbox and deck.gl
viewport: {
width: (2 * window.innerWidth) / 3,
height: window.innerHeight,
Expand All @@ -39,6 +43,7 @@ class Map extends Component {
showStops: true,
selectedStops: [],
subroute: null,
agency: 'muni',
};
}

Expand All @@ -52,20 +57,12 @@ class Map extends Component {
window.removeEventListener('resize', this.updateDimensions.bind(this));
}

/**
* given the two selected stop sids, returns a line segment
* between them
*/
getRouteBetweenStops(routeStops, stops) {
const stopSids = stops.map(stop => stop.sid);
stopSids.sort((a, b) => a - b);
const route = routeStops.map(stop => [stop.lon, stop.lat]);
const startingPointStop = new Stop(routeStops.find(stop => stop.sid === stopSids[0]));
const endingPointStop = new Stop(routeStops.find(stop => stop.sid === stopSids[1]));
/*
* if either value is undefined, it means user selected another stop on another route.
* so clear all stops and subroute
*/
if (startingPointStop.isUndefined() || endingPointStop.isUndefined()) {
this.setState({ subroute: null, selectedStops: [] });
return;
Expand All @@ -77,14 +74,10 @@ class Map extends Component {
this.setState({ subroute, selectedStops: stops });
}

/**
* sets stop sids based on selected stops.
* Stores up to two stops sids. Used to draw subroutes
*/
getStopInfo(route, stopCoordinates) {
let stops = [...this.state.selectedStops];
const station = route.stops.find(currentStop => currentStop.lon === stopCoordinates[0]
&& currentStop.lat === stopCoordinates[1]);
&& currentStop.lat === stopCoordinates[1]);
const stopInfo = new Stop();
stopInfo.setCoordinates(stopCoordinates);
stopInfo.sid = station.sid;
Expand All @@ -102,20 +95,26 @@ class Map extends Component {
}
}

/*
* Change location when selecting another city, passed into ControlPanel
* latitude: coordinate to centre on
* longitude: coordinate to centre on
* zoom: level of zoom to set to (optional)
*/
setMapLocation(latitude, longitude, zoom) {
// ✅ FIXED: accepts city as 4th param, updates agency, notifies App.js
setMapLocation(latitude, longitude, zoom, city) {
const newAgency = agencyMap[city] || this.state.agency;

this.setState({
viewport: Object.assign(this.state.viewport, {
latitude,
longitude,
zoom: zoom || this.state.viewport.zoom,
}),
agency: newAgency,
selectedStops: [],
subroute: null,
});

if (this.props.onAgencyChange) {
this.props.onAgencyChange(newAgency);
}

this.clearSelectedRoutes();
}

updateDimensions() {
Expand All @@ -128,7 +127,6 @@ class Map extends Component {
}

displayVehicleInfo(info) {
/* calls parent' onMarkerClick function to show pop-up to display vehicle id & heading info */
if (info && info.object && info.object.vid && info.object.heading) {
this.setState({
popup: {
Expand Down Expand Up @@ -178,20 +176,25 @@ class Map extends Component {

renderMap() {
const onViewportChange = viewport => this.setState({ viewport });
const { trynState } = this.props.trynState || {};
const { routes } = trynState || {};

// ✅ FIXED: correct destructuring — was previously double-destructuring
const trynState = this.props.trynState || {};
const { routes } = trynState;

const {
viewport, geojson, subroute, selectedStops,
} = this.state;

const subRouteLayer = subroute && getSubRoutesLayer(subroute);
// selectedRouteNames are the route names in the GeoJSON file

const selectedRouteNames = new Set();
this.selectedRoutes
.forEach(route => selectedRouteNames.add(route.properties.name));
// maps API route name to GeoJSON route name

const routeNameMapping = {
KT: 'K/T',
};

const routeLayers = (routes || [])
.filter(route => selectedRouteNames.has(routeNameMapping[route.rid] || route.rid))
.reduce((layers, route) => [
Expand All @@ -205,7 +208,9 @@ class Map extends Component {
subRouteLayer,
...getVehicleMarkersLayer(route, info => this.displayVehicleInfo(info)),
], []);

routeLayers.push(getRoutesLayer(geojson));

return (
<MapGL
{...viewport}
Expand All @@ -216,7 +221,6 @@ class Map extends Component {
<div className="navigation-control">
<NavigationControl onViewportChange={onViewportChange} />
</div>
{/* React Map GL Popup component displays vehicle ID & heading info */}
{this.state.popup.coordinates ? (
<Popup
longitude={this.state.popup.coordinates.lon}
Expand Down Expand Up @@ -248,8 +252,8 @@ class Map extends Component {
<ControlPanel
filterRoutes={route => this.filterRoutes(route)}
toggleStops={() => this.toggleStops()}
setMapLocation={(latitude, longitude, zoom) =>
this.setMapLocation(latitude, longitude, zoom)}
setMapLocation={(latitude, longitude, zoom, city) =>
this.setMapLocation(latitude, longitude, zoom, city)}
refetch={data => this.refetch(data)}
clearSelectedRoutes={() => this.clearSelectedRoutes()}
/>
Expand All @@ -266,6 +270,7 @@ Map.propTypes = {
propTypes.arrayOf(propTypes.object),
]).isRequired,
relay: propTypes.element.isRequired,
onAgencyChange: propTypes.func.isRequired, // ✅ new required prop
};

export default createRefetchContainer(
Expand Down Expand Up @@ -302,4 +307,4 @@ export default createRefetchContainer(
...Map_trynState
}
`,
);
);
21 changes: 7 additions & 14 deletions src/helpers/Route.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,18 @@ const TTC_EXPRESS_COLOR = [0, 165, 79];
/* https://en.wikipedia.org/wiki/Template:MUNI_color */
const MUNI_COLOR = [
{ line: 'E', color: [102, 102, 102] },
// { line: 'F', color: [240, 230, 140] },
{ line: 'J', color: [250, 166, 52] },
{ line: 'KT', color: [86, 155, 190] },
{ line: 'L', color: [146, 39, 143] },
{ line: 'M', color: [0, 135, 82] },
{ line: 'N', color: [0, 83, 155] },
{ line: 'S', color: [255, 204, 0] },
// { line: 'T', color: [211, 18, 69] },
{ line: 'PM', color: [83, 161, 177] },
{ line: 'PH', color: [71, 176, 153] },
{ line: 'C', color: [116, 167, 178] },
{ line: 'default', color: [204, 0, 51] },
];

/* changes color of selected stop along a route */
function selectedStopColor(stop, selectedStops) {
const selectedStopsIds =
selectedStops.map(currentstop => currentstop.sid);
Expand All @@ -44,9 +41,8 @@ function selectedStopColor(stop, selectedStops) {
}
return [255, 0, 0];
}

export function getStopMarkersLayer({ rid, stops }, getStopInfo, selectedStops) {
/* returns new DeckGL Icon Layer displaying all stops on given routes */
// Push stop markers into data array
const data = stops.map(stop => ({
position: [stop.lon, stop.lat],
icon: 'marker',
Expand Down Expand Up @@ -95,7 +91,6 @@ export function getSubRoutesLayer(subroute) {
return subrouteLayer;
}

/* convert line name to color */
function lineToColor(line) {
const checkedLine = MUNI_COLOR.filter(e => e.line === line);
if (checkedLine.length > 0) {
Expand All @@ -109,7 +104,6 @@ function lineToColor(line) {
}

export function getVehicleMarkersLayer(route, displayVehicleInfo) {
/* return a new IconLayer */
function newIconLayer(id, data, iconAtlas) {
return new IconLayer({
id,
Expand All @@ -122,37 +116,36 @@ export function getVehicleMarkersLayer(route, displayVehicleInfo) {
iconAtlas,
iconMapping: BUS_ICON_MAPPING,
pickable: true,
// calls pop-up function
onClick: info => displayVehicleInfo(info),
});
}

/* returns new DeckGL Icon Layer displaying all vehicles on given routes */
// ✅ West-facing buses (heading >= 180): use busIconWest image
// angle formula unified to (90 - heading) for correct North-clockwise mapping
const westData = route.routeStates[0].vehicles.reduce((westBus, vehicle) => {
if (vehicle.heading >= 180) {
westBus.push({
position: [vehicle.lon, vehicle.lat],
icon: 'marker',
size: 65,
angle: 270 - vehicle.heading,
angle: 90 - vehicle.heading, // ✅ FIXED: was (270 - vehicle.heading)
color: lineToColor(route.rid),
// added vid & heading info to display onClick pop-up
vid: vehicle.vid,
heading: vehicle.heading,
});
}
return westBus;
}, []);

// ✅ East-facing buses (heading < 180): formula was already correct
const eastData = route.routeStates[0].vehicles.reduce((eastBus, vehicle) => {
if (vehicle.heading < 180) {
eastBus.push({
position: [vehicle.lon, vehicle.lat],
icon: 'marker',
size: 64,
angle: 90 - vehicle.heading,
angle: 90 - vehicle.heading, // ✅ unchanged — already correct
color: lineToColor(route.rid),
// added vid & heading info to display onClick pop-up
vid: vehicle.vid,
heading: vehicle.heading,
});
Expand All @@ -164,4 +157,4 @@ export function getVehicleMarkersLayer(route, displayVehicleInfo) {
newIconLayer(route.rid.concat('west-vehicle-icon-layer'), westData, busIconWest),
newIconLayer(route.rid.concat('east-vehicle-icon-layer'), eastData, busIconEast),
];
}
}