Skip to content

Instantly share code, notes, and snippets.

@brense
Last active April 12, 2023 11:38
Show Gist options
  • Save brense/bafdbcf0b656bb101be3ab5c31242ea9 to your computer and use it in GitHub Desktop.
Save brense/bafdbcf0b656bb101be3ab5c31242ea9 to your computer and use it in GitHub Desktop.
Travel assistent project i've been working on recently
import fetch from 'node-fetch'
import Trip from '../customTypes/Trip'
import Station, { StationType } from '../customTypes/Station'
import moment from 'moment'
const { NS_API_PRIMARY_KEY } = process.env
const headers = { ['Ocp-Apim-Subscription-Key' as string]: NS_API_PRIMARY_KEY || '' }
const apiUrl = 'https://gateway.apiportal.ns.nl'
// temporary use a cache to prevent excess requests to NS API
const tempCache: any = {}
export async function getPlaces(q?: string, station_code?: string, lat?: number, long?: number) {
const find = q ? { q } : station_code ? { station_code } : { lat, lng: long }
const response = await fetch(`${apiUrl}/places-api/v2/places?limit=100&${serialize(find)}&radius=5000&lang=nl&type=stationV2`, { headers }) // removed: stop,address
const data: { payload: any[] } = await response.json()
return data.payload.reduce((places, { type, locations }) => {
locations.forEach(({ stationCode, UICCode, link, name, lat, lng, ...rest }: any) => places.push({ ...rest, name, UICCode, stationCode, link: link.uri, type, position: { lat, long: lng } }))
return places
}, [])
}
export async function getStations() {
const response = await fetch(`${apiUrl}/reisinformatie-api/api/v2/stations`, { headers })
const data: { payload: any[] } = await response.json()
return data.payload.map(mapStationPayload)
}
type TripArgs = {
from: string,
to: string,
dateTime?: Date,
searchForArrival?: boolean,
excludeTrainsWithReservationRequired?: boolean,
searchForAccessibleTrip?: boolean,
localTrainsOnly?: boolean,
addChangeTime?: number
}
export async function getTrips({ from, to, dateTime, searchForArrival, excludeTrainsWithReservationRequired, localTrainsOnly, searchForAccessibleTrip, addChangeTime }: TripArgs) {
const formatted = moment(dateTime).format('YYYY-MM-DDTHH:mm:00')
try {
const url = `${apiUrl}/reisinformatie-api/api/v3/trips?
passing=true&
fromStation=${from}&
toStation=${to}&
dateTime=${formatted}&
searchForArrival=${typeof searchForArrival !== 'undefined' ? searchForArrival : false}&
searchForAccessibleTrip=${typeof searchForAccessibleTrip !== 'undefined' ? searchForAccessibleTrip : false}&
excludeTrainsWithReservationRequired=${typeof excludeTrainsWithReservationRequired !== 'undefined' ? excludeTrainsWithReservationRequired : true}&
localTrainsOnly=${typeof localTrainsOnly !== 'undefined' ? localTrainsOnly : false}&
addChangeTime=${typeof addChangeTime !== 'undefined' ? addChangeTime : 0}
`.replace(/[ +\r\n\t]/g, '')
console.log('URL', url)
if (tempCache[url]) {
return tempCache[url]
} else {
const response = await fetch(url, { headers })
const data: { trips: any[] } = await response.json()
if (response.status !== 200) {
throw new Error('')
}
const mapped = data.trips.map(mapTripPayload)
tempCache[url] = mapped
return mapped
}
} catch (e) {
console.log(e)
}
}
function mapTripPayload(trip: any): Trip {
const { legs, ...rest } = trip
return {
...rest,
legs: legs.map(({ stops, destination, origin, ...leg }: any) => {
return {
...leg,
destination: {
...destination,
position: {
lat: destination.lat,
long: destination.lng
}
},
origin: {
...origin,
position: {
lat: origin.lat,
long: origin.lng
}
},
stops: stops.map(({ actualDepartureDateTime, actualArrivalDateTime, ...stop }: any) => {
return {
...stop,
actualDepartureDateTime: moment(actualDepartureDateTime).toDate(),
actualArrivalDateTime: moment(actualArrivalDateTime).toDate()
}
})
}
})
}
}
function mapStationPayload(station: any): Station {
const {
UICCode,
stationType,
EVACode,
code,
sporen,
synoniemen,
heeftFaciliteiten,
heeftVertrektijden,
heeftReisassistentie,
namen,
land,
lat,
lng,
radius,
naderenRadius,
ingangsDatum
} = station
return {
code,
UICCode,
EVACode,
stationType: StationType[stationType as keyof typeof StationType],
tracks: sporen.map(({ spoorNummer }: any) => ({ number: spoorNummer })),
synonyms: synoniemen,
hasFacilities: heeftFaciliteiten,
hasDepartures: heeftVertrektijden,
hasTravelAssistance: heeftReisassistentie,
names: { long: namen.lang, medium: namen.middel, short: namen.kort },
country: land,
position: { lat, long: lng },
radius,
approachRadius: naderenRadius,
startDate: new Date(ingangsDatum)
}
}
function serialize(obj: any) {
const str = []
for (const p in obj)
if (obj.hasOwnProperty(p)) {
str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]))
}
return str.join('&')
}
import { useLazyQuery } from '@apollo/client'
import { TextField } from '@material-ui/core'
import { Autocomplete } from '@material-ui/lab'
import React, { useCallback, useEffect, useMemo } from 'react'
import { QUERY_PLACES } from '../queries'
let timeout: NodeJS.Timeout
function PlaceSearch<T extends { name: string, type: string }>({ endAdornment, label, margin, value, onChange }: { endAdornment?: React.ReactNode, label: React.ReactNode, margin?: 'dense' | 'none' | 'normal', value: T | null, onChange: (event: React.ChangeEvent<{}>, value: T | null) => void }) {
const [getPlaces, { loading, data }] = useLazyQuery<{ places: T[] }>(QUERY_PLACES)
useEffect(() => {
return function cleanup() {
clearTimeout(timeout)
}
}, [])
const search = useCallback((q: string) => {
if (value) {
getPlaces({ variables: { q: value.name } })
} else {
clearTimeout(timeout)
timeout = setTimeout(() => {
if (q.length > 2) {
getPlaces({ variables: { q } })
}
}, 500)
}
}, [getPlaces, value])
const filteredOptions = useMemo(() => {
if (data) {
const stations = data.places.filter(({ type }) => type === 'stationV2')
return stations.length > 0 ? stations : data.places
}
return []
}, [data])
return <Autocomplete
renderInput={(props) => (
<TextField
{...props}
label={label}
margin={margin}
variant="outlined"
InputProps={{ ...props.InputProps, style: { paddingRight: 8 }, endAdornment }}
/>
)}
value={value}
loading={loading}
loadingText="Zoeken..."
noOptionsText="Zoek naar een station"
onChange={onChange}
onInputChange={(e, v) => search(v)}
options={filteredOptions}
getOptionLabel={opt => opt.name}
getOptionSelected={(opt, v) => opt.name === v.name}
/>
}
export default PlaceSearch
import 'reflect-metadata'
import 'dotenv/config'
import { createConnection } from 'typeorm'
import * as express from 'express'
import * as bodyParser from 'body-parser'
import { ApolloServer } from 'apollo-server-express'
import { createServer } from 'http'
import { buildSchema } from 'type-graphql'
import { verfifyAuthBearer, objectToArray } from './utils'
import * as resolversObj from './resolvers'
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
(async () => {
// init database connection
await createConnection()
// init express app
const app = express()
// serve static files
app.use('/static', express.static(__dirname + '/static'))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.get('*', (req, res, next) => {
const path = __dirname + '/index.html'
res.sendFile(path)
})
// build graphql schema
const resolvers = objectToArray(resolversObj) as [Function, ...Function[]]
const schema = await buildSchema({ resolvers })
// init apollo server
const server = new ApolloServer({
schema,
context,
introspection: true,
playground: true,
subscriptions: {
path: '/graphql',
keepAlive: 10000,
onConnect
}
})
// apply middleware and start server
server.applyMiddleware({ app, path: '/graphql' })
const httpServer = createServer(app)
server.installSubscriptionHandlers(httpServer)
httpServer.listen(port, '0.0.0.0', () => {
console.log(`Server ready at: http://localhost:${port}${server.graphqlPath}, subscriptions: ws://localhost:${port}${server.subscriptionsPath}`)
})
})()
async function context({ req }: { req: express.Request }) {
// TODO: get additional user info?
return {
user: await verfifyAuthBearer(req.headers.authorization)
}
}
function onConnect(connectionParams: { authorization?: string }) {
return verfifyAuthBearer(connectionParams.authorization)
}
import { Checkbox, Dialog, DialogContent, DialogProps, DialogTitle as MuiDialogTitle, DialogTitleProps, Divider, FormControlLabel, Icon, IconButton, makeStyles, Radio, RadioGroup, Slide, Slider, Typography } from '@material-ui/core'
import { TransitionProps } from '@material-ui/core/transitions';
import React, { useCallback, useContext } from 'react'
import { useHistory } from 'react-router-dom';
import { AppContext } from '../App';
const Transition = React.forwardRef(function Transition(
props: TransitionProps & { children?: React.ReactElement<any, any> },
ref: React.Ref<unknown>,
) {
return <Slide direction="up" ref={ref} {...props} />
})
const useStyles = makeStyles(theme => ({
root: {
margin: 0,
padding: theme.spacing(2)
},
closeButton: {
position: 'absolute',
right: theme.spacing(1),
top: theme.spacing(1),
color: theme.palette.grey[500]
}
}))
function DialogTitle(props: DialogTitleProps & { onClose: () => void }) {
const classes = useStyles()
const { children, onClose, ...other } = props
return (
<MuiDialogTitle disableTypography {...other} className={classes.root}>
<Typography variant="h6">{children}</Typography>
{onClose ? (
<IconButton aria-label="close" onClick={onClose} className={classes.closeButton}>
<Icon>close</Icon>
</IconButton>
) : null}
</MuiDialogTitle>
)
}
const marks = [
{ value: 0, label: '0' },
{ value: 5, label: '+5' },
{ value: 10, label: '+10' },
{ value: 15, label: '+15' },
{ value: 20, label: '+20' }
]
function SettingsDialog(props: DialogProps) {
const { preferences, setPreferences } = useContext(AppContext)
const history = useHistory()
const handleChange = useCallback((property: string, value: any) => {
setPreferences({ ...preferences, [property]: value })
}, [preferences, setPreferences])
return <Dialog {...props} TransitionComponent={Transition}>
<DialogTitle onClose={() => history.goBack()}>Voorkeuren</DialogTitle>
<Divider />
<DialogContent style={{ flex: 'none' }}>
<Typography variant="body2" gutterBottom={true}>Geef voorkeur aan:</Typography>
<RadioGroup name="travel-options" value={preferences.connection} onChange={v => handleChange('connection', v.target.value as 'fastest')}>
<FormControlLabel value="fastest" control={<Radio />} label="Snelste verbinding" />
<FormControlLabel value="transfers" control={<Radio />} label="Minste overstappen" />
</RadioGroup>
</DialogContent>
<Divider />
<DialogContent style={{ flex: 'none' }}>
<FormControlLabel
control={<Checkbox checked={preferences.fewerStops} onChange={v => handleChange('fewerStops', v.target.checked)} name="fewerStops" />}
label="Zo min mogelijk tussenstops"
/>
</DialogContent>
<Divider />
<DialogContent style={{ flex: 'none' }}>
<FormControlLabel
control={<Checkbox checked={preferences.excludeTrainsWithReservationRequired} onChange={v => handleChange('excludeTrainsWithReservationRequired', v.target.checked)} name="excludeTrainsWithReservationRequired" />}
label="Geen treinen met verplichte reservering"
/>
</DialogContent>
<Divider />
<DialogContent style={{ flex: 'none' }}>
<FormControlLabel
control={<Checkbox checked={preferences.localTrainsOnly} onChange={v => handleChange('localTrainsOnly', v.target.checked)} name="localTrainsOnly" />}
label="Reis alleen met stoptrein(en)"
/>
</DialogContent>
<Divider />
<DialogContent style={{ flex: 'none' }}>
<FormControlLabel
control={<Checkbox checked={preferences.searchForAccessibleTrip} onChange={v => handleChange('searchForAccessibleTrip', v.target.checked)} name="searchForAccessibleTrip" />}
label="Toegankelijk reizen"
/>
</DialogContent>
<Divider />
<DialogContent style={{ flex: 'none' }}>
<Typography variant="body2" gutterBottom={true}>Aantal minuten overstaptijd:</Typography>
<Slider
value={preferences.addChangeTime}
onChange={(e, v) => handleChange('addChangeTime', v as number)}
min={0}
max={20}
step={5}
marks={marks}
valueLabelDisplay="off"
color="secondary"
/>
</DialogContent>
<Divider />
</Dialog>
}
export default SettingsDialog
/// <reference types="googlemaps" />
import React, { useCallback, useEffect, useState } from 'react'
import { GoogleMap, LoadScript } from '@react-google-maps/api'
import { Step, Trip } from '../types'
import { Box, Divider, Icon, LinearProgress, Typography } from '@material-ui/core'
import moment from 'moment'
import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab'
const { REACT_APP_GOOGLE_API_KEY } = process.env
type TrackerProps = {
timeToStation?: number,
distanceToStation?: number,
mode: 'walking' | 'bicycling' | 'driving',
onModeChange: (mode: 'walking' | 'bicycling' | 'driving') => void,
trips: Trip[], start: Step, finish: Step, when: 'departure' | 'arrival'
}
const modes = {
'walking': { label: 'lopen', icon: 'directions_walk' },
'bicycling': { label: 'fietsen', icon: 'directions_bike' },
'driving': { label: 'rijden', icon: 'directions_car' }
}
let directionsService: google.maps.DirectionsService
let directionsRenderer: google.maps.DirectionsRenderer
let myLocation: google.maps.Marker
function Tracker({ trips, start, finish, when, timeToStation, distanceToStation, onModeChange, mode }: TrackerProps) {
const [loading, setLoading] = useState(true)
useEffect(() => {
navigator.geolocation.watchPosition(position => {
// TODO: check distance with start location, if distance less than 10m switch to: did you miss your train?
myLocation && myLocation.setPosition({ lat: position.coords.latitude, lng: position.coords.longitude })
})
}, [])
const handleModeChange = useCallback((mode: keyof typeof modes, updateParent = false) => {
setLoading(true)
if (updateParent) {
onModeChange(mode)
}
navigator.geolocation.getCurrentPosition(position => {
const request = {
origin: new google.maps.LatLng(position.coords.latitude, position.coords.longitude),
destination: new google.maps.LatLng(start.origin?.position.lat || 0, start.origin?.position.long || 0),
travelMode: mode.toUpperCase() as google.maps.TravelMode
}
directionsService.route(request, function (result: any, status: any) {
if (status === 'OK') {
console.log(result)
directionsRenderer.setDirections(result)
setLoading(false)
}
})
})
}, [start, onModeChange])
const handleMapLoad = useCallback((map: any) => {
directionsService = new google.maps.DirectionsService()
directionsRenderer = new google.maps.DirectionsRenderer()
directionsRenderer.setMap(map)
myLocation = new google.maps.Marker({
clickable: false,
zIndex: 999,
map
})
handleModeChange(mode)
}, [handleModeChange, mode])
return <Box height="100%" display="flex" flexDirection="column">
<Box flex={1}>
<LoadScript googleMapsApiKey={REACT_APP_GOOGLE_API_KEY || ''}>
<GoogleMap
mapContainerStyle={{ width: '100%', height: '100%' }}
zoom={10}
onLoad={handleMapLoad}
>
</GoogleMap>
</LoadScript>
</Box>
{loading ? <LinearProgress color="secondary" variant="indeterminate" /> : <Divider />}
<Box padding={2} display="flex" flexDirection="column" alignItems="center">
<ToggleButtonGroup value={mode} onChange={(e, v) => handleModeChange(v, true)} exclusive={true} style={{ marginBottom: 16 }}>
{Object.keys(modes).map(key => <ToggleButton key={key} value={key}>
<Icon>{modes[key as keyof typeof modes].icon}</Icon>&nbsp;&nbsp;
{modes[key as keyof typeof modes].label}
</ToggleButton>)}
</ToggleButtonGroup>
<Typography>{moment.duration(timeToStation || 0, 'seconds').humanize()} {modes[mode].label} naar station {start.origin?.name}</Typography>
</Box>
</Box>
}
export default Tracker
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment