Skip to content

Commit

Permalink
Toasters on a map (#81)
Browse files Browse the repository at this point in the history
* added LEAP to drop down

UrbanOS-Public/smartcitiesdata#487

co-authored-by: Scott Millard <smillard@hntb.com>

* handling leap position updates in reducer

UrbanOS-Public/smartcitiesdata#487

co-authored-by: Scott Millard <smillard@hntb.com>

* using correct icon for LEAP; duplicated websocket saga for LEAP

UrbanOS-Public/smartcitiesdata#487

co-authored-by: mcsearchin <abrock17@gmail.com>

* DRYd up gee 2 web socket sagas

UrbanOS-Public/smartcitiesdata#487

co-authored-by: Scott Millard <smillard@hntb.com>

Co-authored-by: smillardHNTB <37001890+smillardHNTB@users.noreply.github.com>
  • Loading branch information
mcsearchin and ScottMillard authored Feb 5, 2020
1 parent 42e2610 commit 9d9ea34
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 37 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"start": "webpack-dev-server --mode development --open",
"test": "jest",
"test-watch": "jest --watch",
"dev": "webpack --mode development",
"build": "webpack --mode production"
},
Expand Down Expand Up @@ -71,6 +72,7 @@
"moduleNameMapper": {
"\\.(css|scss|png)$": "identity-obj-proxy",
"blue-bus\\.svg": "<rootDir>/test-helpers/blue-svg.js",
"map-marker-easymile\\.svg": "<rootDir>/test-helpers/map-marker-easymile-svg.js",
"\\.(svg)$": "<rootDir>/test-helpers/mock-svg-string.js"
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/actions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

export const POSITION_UPDATE = 'POSITION_UPDATE'

export const LEAP_POSITION_UPDATE = 'LEAP_POSITION_UPDATE'

export const ROUTE_FILTER = 'ROUTE_FILTER'

export const ROUTE_FETCH = 'ROUTE_FETCH'
Expand All @@ -11,6 +13,10 @@ export const positionUpdate = (message) => {
return { type: POSITION_UPDATE, update: message }
}

export const leapPositionUpdate = (message) => {
return { type: LEAP_POSITION_UPDATE, update: message }
}

export const applyStreamFilter = (filter) => {
return { type: ROUTE_FILTER, filter: filter }
}
Expand Down
1 change: 1 addition & 0 deletions src/assets/map-marker-easymile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/components/cota-position-map/icon-factory.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import leaflet from 'leaflet'
import busBlueSvg from '../../assets/blue-bus.svg'
import easymileSvg from '../../assets/map-marker-easymile.svg'
import locationPin from '../../assets/ic_location-dot.svg'
import { LEAP } from '../../variables'

const createBusIcon = (zoomLevel, provider) => {
let iconUrl = busBlueSvg
let iconSize = [3.2 * zoomLevel, 2.75 * zoomLevel]

if (LEAP == provider) {
iconUrl = easymileSvg
iconSize = [2 * zoomLevel, 2 * zoomLevel]
}

return leaflet.icon({
iconUrl: iconUrl,
iconSize: iconSize
Expand Down
10 changes: 10 additions & 0 deletions src/components/cota-position-map/icon-factory.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import leaflet from 'leaflet'
import busBlueSvg from '../../assets/blue-bus.svg'
import easymileSvg from '../../assets/map-marker-easymile.svg'
import iconFactory from './icon-factory'
import locationPin from '../../assets/ic_location-dot.svg'

Expand All @@ -25,4 +26,13 @@ describe('Icon Factory', () => {
iconSize: [32, 27.5]
})
})

it('creates an easymile icon for easymile route', () => {
iconFactory.createBusIcon('10', 'LEAP')

expect(leaflet.icon).toHaveBeenCalledWith({
iconUrl: easymileSvg,
iconSize: [20, 20]
})
})
})
26 changes: 21 additions & 5 deletions src/reducers/reducers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { combineReducers } from 'redux'
import { POSITION_UPDATE, ROUTE_FILTER, ROUTE_UPDATE } from '../actions'
import { COTA } from '../variables'
import { POSITION_UPDATE, LEAP_POSITION_UPDATE, ROUTE_FILTER, ROUTE_UPDATE } from '../actions'
import { COTA, LEAP } from '../variables'
import _ from 'lodash'

const filter = (filter = [], action) => {
Expand All @@ -15,6 +15,9 @@ const filter = (filter = [], action) => {
const provider = (provider = { name: COTA }, action) => {
switch (action.type) {
case ROUTE_FILTER:
if(action.filter[0] == LEAP) {
return Object.assign({}, provider, { name: LEAP })
}
return Object.assign({}, provider, { name: COTA })
default:
return provider
Expand All @@ -36,24 +39,37 @@ const data = (data = {}, action) => {
}

return Object.assign({}, data, { [value.vehicleId]: value })
case LEAP_POSITION_UPDATE:
let attributes = action.update.attributes
let leap_value = {
vehicleId: attributes.vehicle_id,
latitude: attributes.lat,
longitude: attributes.lon,
bearing: 0,
provider: LEAP
}

return Object.assign({}, data, { [leap_value.vehicleId]: leap_value })
case ROUTE_FILTER:
return {}
default:
return data
}
}

const availableRoutes = (availableRoutes = [], action) => {
const defaultRoutes = [{ value: LEAP, label: 'SMRT - Linden LEAP', provider: LEAP }]

const availableRoutes = (availableRoutes = defaultRoutes, action) => {
switch (action.type) {
case ROUTE_UPDATE:
const sorted = _.sortBy(action.update, ({ linenum }) => linenum);
const uniqueRoutes = _.sortedUniqBy(sorted, ({ linenum }) => linenum)
let routesToUse = uniqueRoutes.map((route) => {
const lineNumber = new String(route.linenum).padStart(3, '0')
const lineName = `${route.linenum} - ${route.linename}`
return { value: lineNumber, label: lineName, provider: 'COTA' }
return { value: lineNumber, label: lineName, provider: COTA }
})
return routesToUse
return [...routesToUse, ...defaultRoutes]
default:
return availableRoutes
}
Expand Down
67 changes: 65 additions & 2 deletions src/reducers/reducers.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import reducer from './index'
import { POSITION_UPDATE, ROUTE_FILTER, ROUTE_FETCH, ROUTE_UPDATE } from '../actions'
import { POSITION_UPDATE, LEAP_POSITION_UPDATE, ROUTE_FILTER, ROUTE_FETCH, ROUTE_UPDATE } from '../actions'
import { COTA, LEAP } from '../variables'

describe('cotaApp reducers', () => {
it('will save the filter when processing a ROUTE_FILTER action', () => {
Expand Down Expand Up @@ -127,7 +128,7 @@ describe('cotaApp reducers', () => {
expect(newState.availableRoutes).toEqual([{ value: '001', label: '1 - Crazy Town' }])
})

it('will should not remove all availableRoutes on route fetch action', () => {
it('will not remove all availableRoutes on route fetch action', () => {
const availableRoutes = [
{ value: '001', label: '1 - Crazy Town' },
{ value: '101', label: '101 - Smallville' }
Expand All @@ -141,4 +142,66 @@ describe('cotaApp reducers', () => {
let newState = reducer(currentState, { type: ROUTE_FETCH })
expect(newState.availableRoutes).toEqual(availableRoutes)
})

it('sets the provider to COTA by default for all ROUTE_FILTER actions', () => {
let newState = reducer(undefined, { type: 'ROUTE_FILTER', filter: ['003'] })

expect(newState.provider).toEqual({ name: COTA })
})

describe('LEAP', () => {
it('has the LEAP route by default', () => {
let newState = reducer(undefined, { type: 'UNKNOWN_ACTION', stuff: [] })

expect(newState.availableRoutes).toEqual([{ value: 'LEAP', label: 'SMRT - Linden LEAP', provider: 'LEAP' }])
})

it('has the LEAP route last after routes are updated', () => {
const message = [{ value: '001', label: '1 - Crazy Town', provider: 'COTA' }]
let newState = reducer(undefined, { type: ROUTE_UPDATE, update: message })

expect(newState.availableRoutes[newState.availableRoutes.length - 1])
.toEqual({ value: 'LEAP', label: 'SMRT - Linden LEAP', provider: 'LEAP' })
})

it('transforms the data on a LEAP_POSITION_UPDATE action', () => {
const state = {
data: {
'SIMUPONYTAvehicle_api-test-public-v3-2': {
'stuff': 'that-should-not-be-changed'
}
}
}
const message = {
'attributes': {
'vehicle_id': 'SIMUPONYTAvehicle_api-test-public-v3-5',
'lat': 43.538284742935346,
'lon': 1.3600251758143491
}
}

const expected_data = {
...state.data,
...{
'SIMUPONYTAvehicle_api-test-public-v3-5': {
vehicleId: 'SIMUPONYTAvehicle_api-test-public-v3-5',
latitude: 43.538284742935346,
longitude: 1.3600251758143491,
bearing: 0,
provider: 'LEAP'
}
}
}

let newState = reducer(state, { type: LEAP_POSITION_UPDATE, update: message })

expect(newState.data).toEqual(expected_data)
})

it('sets the provider to LEAP when ROUTE_FILTER has LEAP filter', () => {
let newState = reducer(undefined, { type: 'ROUTE_FILTER', filter: ['LEAP'] })

expect(newState.provider).toEqual({ name: LEAP })
})
})
})
31 changes: 5 additions & 26 deletions src/sagas/websocket.js → src/sagas/cotaWebSocket.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import "regenerator-runtime/runtime";
import { call, take, put, race, select } from 'redux-saga/effects'
import { eventChannel } from 'redux-saga'
import { Socket } from 'phoenix'
import * as socketUtils from './websocket-utils'
import { ROUTE_FILTER, positionUpdate } from '../actions'
import { COTA } from '../variables'

Expand All @@ -13,12 +12,6 @@ import { COTA } from '../variables'
*/
let localStateFilters = []

export let createSocket = (socketUrl) => {
let socket = new Socket(socketUrl)
socket.connect()
return socket
}

const createChannel = function* (socket) {
const channel = socket.channel('streaming:central_ohio_transit_authority__cota_stream', { 'vehicle.trip.route_id': [] })
localStateFilters = yield select(state => state.filter)
Expand All @@ -36,21 +29,6 @@ const sendFilter = (channel) => {
channel.push('filter', filter)
}

const unsubscribe = () => { }

const createEventChannel = channel => {
return eventChannel(emit => {
channel.on('update', emit)

channel.join()
.receive('ok', () => console.log('Connection Successful'))
.receive('error', ({ reason }) => console.log('failed join', reason))
.receive('timeout', () => console.log('Networking issue. Still waiting...'))

return unsubscribe
})
}

const fromServer = function* (eventChannel) {
while (true) {
const message = yield take(eventChannel)
Expand All @@ -72,14 +50,15 @@ const fromEventBus = function* (channel) {
}

const doSaga = function* () {
const socket = yield call(createSocket, `${window.WEBSOCKET_HOST}/socket`)
localStateFilters = yield select(state => state.filter)
const socket = yield call(socketUtils.createSocket, `${window.WEBSOCKET_HOST}/socket`)
const channel = yield call(createChannel, socket)
const eventChannel = yield call(createEventChannel, channel)
const eventChannel = yield call(socketUtils.createEventChannel, channel)

yield race([call(fromEventBus, channel), call(fromServer, eventChannel)])
}

export default function* websocketSaga() {
export default function* cotaWebSocketSaga() {
while (true) {
const action = yield take(ROUTE_FILTER)
yield call(doSaga, action)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Socket } from 'phoenix'
import sagas from './websocket'
import sagas from './cotaWebSocket'
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { POSITION_UPDATE, ROUTE_FILTER } from '../actions'
Expand Down
6 changes: 4 additions & 2 deletions src/sagas/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import webSocketSaga from './websocket'
import cotaWebSocketSaga from './cotaWebSocket'
import leapWebSocketSaga from './leapWebSocket'
import routeSaga from './route'
import { fork, all } from 'redux-saga/effects'

export default function* allSagas() {
yield all([
fork(webSocketSaga),
fork(cotaWebSocketSaga),
fork(leapWebSocketSaga),
fork(routeSaga)
])
}
46 changes: 46 additions & 0 deletions src/sagas/leapWebSocket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { call, take, put, race, select } from 'redux-saga/effects'
import * as socketUtils from './websocket-utils'
import { leapPositionUpdate, ROUTE_FILTER } from '../actions'
import { LEAP } from '../variables'

const createChannel = (socket) => {
return socket.channel('streaming:easymile__linden_positions', {})
}

const fromServer = function* (eventChannel) {
while (true) {
const message = yield take(eventChannel)
if (message !== undefined) {
message.provider = LEAP
}

let provider = yield select(state => state.provider.name)
if (provider === LEAP) {
yield put(leapPositionUpdate(message))
}
}
}

const fromEventBus = function* (channel) {
while (true) {
const action = yield take(ROUTE_FILTER)
if (LEAP === action.filter[0]) {
channel.push('filter', {})
}
}
}

const doSaga = function* () {
const socket = yield call(socketUtils.createSocket, `${window.WEBSOCKET_HOST}/socket`)
const channel = yield call(createChannel, socket)
const eventChannel = yield call(socketUtils.createEventChannel, channel)

yield race([call(fromEventBus, channel), call(fromServer, eventChannel)])
}

export default function* leapWebSocketSaga() {
while (true) {
const action = yield take(ROUTE_FILTER)
yield call(doSaga, action)
}
}
30 changes: 30 additions & 0 deletions src/sagas/websocket-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { eventChannel } from 'redux-saga'
import { Socket } from 'phoenix'

export const createSocket = (socketUrl) => {
let socket = new Socket(socketUrl)
socket.connect()
return socket
}

export const createChannel = (socket, channelName, filter) => {
const channel = socket.channel(channelName, filter)
socket.onOpen(() => channel.push('filter', filter))

return channel
}

export const createEventChannel = channel => {
return eventChannel(emit => {
channel.on('update', emit)

channel.join()
.receive('ok', () => console.log('Connection Successful'))
.receive('error', ({ reason }) => console.log('failed join', reason))
.receive('timeout', () => console.log('Networking issue. Still waiting...'))

return unsubscribe
})
}

const unsubscribe = () => { }
3 changes: 2 additions & 1 deletion src/variables.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const COTA = 'COTA'
export const COTA = 'COTA'
export const LEAP = 'LEAP'
2 changes: 2 additions & 0 deletions test-helpers/map-marker-easymile-svg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const mockString = '<svg>easymile</svg>'
export default mockString

0 comments on commit 9d9ea34

Please sign in to comment.