-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #196 from CityScope/feature/websockets
WIP: websockets
- Loading branch information
Showing
24 changed files
with
1,025 additions
and
197 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
REACT_APP_MAPBOX_TOKEN=pk.eyJ1IjoicmVsbm94IiwiYSI6ImNqd2VwOTNtYjExaHkzeXBzYm1xc3E3dzQifQ.X8r8nj4-baZXSsFgctQMsg | ||
PUBLIC_URL=https://cityscope.media.mit.edu/CS_cityscopeJS | ||
PUBLIC_URL=https://cityio.media.mit.edu | ||
SKIP_PREFLIGHT_CHECK=true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,160 +1,183 @@ | ||
/* eslint-disable react-hooks/exhaustive-deps */ | ||
import { useEffect, useState } from "react"; | ||
import { cityIOSettings, generalSettings } from "../../settings/settings"; | ||
import { cityIOSettings } from "../../settings/settings"; | ||
import { | ||
updateCityIOdata, | ||
toggleCityIOisDone, | ||
} from "../../redux/reducers/cityIOdataSlice"; | ||
import { useSelector, useDispatch } from "react-redux"; | ||
import { getAPICall } from "../../utils/utils"; | ||
import useWebSocket, { ReadyState } from "react-use-websocket" | ||
import LoadingProgressBar from "../LoadingProgressBar"; | ||
|
||
const removeElement = (array, elem) => { | ||
var index = array.indexOf(elem); | ||
if (index > -1) { | ||
array.splice(index, 1); | ||
} | ||
return array; | ||
}; | ||
|
||
const CityIO = (props) => { | ||
|
||
const verbose = true; // set to true to see console logs | ||
const waitTimeMS = 5000; | ||
const dispatch = useDispatch(); | ||
const cityIOdata = useSelector((state) => state.cityIOdataState.cityIOdata); | ||
const cityscopeProjectURL = generalSettings.csjsURL; | ||
const { tableName } = props; | ||
const [mainHash, setMainHash] = useState(null); | ||
const [hashes, setHashes] = useState({}); | ||
const possibleModules = cityIOSettings.cityIO.cityIOmodules.map(module => module.name) | ||
const [arrLoadingModules, setArrLoadingModules] = useState([]); | ||
const cityioURL = `${cityIOSettings.cityIO.baseURL}table/${tableName}/`; | ||
|
||
// test if cityIO is up and this table exists | ||
// Creation of the websocket connection. TODO: change WS_URL to env or property | ||
// sendJsonMessage: function that sends a message through the websocket channel | ||
// lastJsonMessage: object that contains the last message received through the websocket | ||
// readyState: indicates whether the WS is ready or not | ||
const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket( | ||
cityIOSettings.cityIO.websocketURL, | ||
{ | ||
share: true, | ||
shouldReconnect: () => true, | ||
}, | ||
) | ||
|
||
// When the WS connection state (readyState) changes to OPEN, | ||
// the UI sends a LISTEN (SUBSCRIBE) message to CityIO with the tableName prop | ||
useEffect(() => { | ||
const testCityIO = async () => { | ||
let test = await getAPICall(cityioURL + "meta/"); | ||
if (test) { | ||
// start fetching API hashes to check for new data | ||
getCityIOmetaHash(); | ||
verbose && | ||
console.log( | ||
"%c cityIO is up, reading cityIO every " + | ||
cityIOSettings.cityIO.interval + | ||
"ms", | ||
"color: red" | ||
); | ||
} else { | ||
setArrLoadingModules([ | ||
`cityIO might be down, please check { ${tableName} } is correct. Returning to cityScopeJS at ${cityscopeProjectURL} in ${ | ||
waitTimeMS / 1000 | ||
} seconds`, | ||
]); | ||
|
||
new Promise((resolve) => { | ||
setTimeout(() => { | ||
window.location.assign(cityscopeProjectURL); | ||
}, waitTimeMS); | ||
resolve(); | ||
}); | ||
} | ||
}; | ||
testCityIO(); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [cityioURL]); | ||
|
||
/** | ||
* gets the main hash of this cityIO table | ||
* on a constant loop to check for updates | ||
*/ | ||
async function getCityIOmetaHash() { | ||
// recursively get hashes | ||
await getAPICall(cityioURL + "meta/id/").then(async (res) => { | ||
// is it a new hash? | ||
if (mainHash !== res) { | ||
setMainHash(res); | ||
} | ||
}); | ||
// do it forever | ||
setTimeout(getCityIOmetaHash, cityIOSettings.cityIO.interval); | ||
} | ||
console.log("Connection state changed") | ||
if (readyState === ReadyState.OPEN) { | ||
sendJsonMessage({ | ||
type: "LISTEN", | ||
content: { | ||
gridId: tableName, | ||
}, | ||
}) | ||
setArrLoadingModules([ | ||
`Loading ${tableName} data.`, | ||
]); | ||
} | ||
}, [readyState]) | ||
|
||
|
||
// When a new WebSocket message is received (lastJsonMessage) the UI checks | ||
// the type of the message and performs the suitable operation | ||
useEffect(() => { | ||
//! only update if hashId changes | ||
if (!mainHash) { | ||
return; | ||
|
||
if(lastJsonMessage == null) return; | ||
console.log(`Got a new message: ${JSON.stringify(lastJsonMessage)}`) | ||
|
||
let messageType = lastJsonMessage.type; | ||
|
||
// If the message is of type GRID, the UI updates the GEOGRID and | ||
// GEOGRIDDATA, optionally, CityIO can send saved modules | ||
if (messageType === 'GRID'){ | ||
verbose && console.log( | ||
` --- trying to update GEOGRID --- ${JSON.stringify(lastJsonMessage.content)}` | ||
); | ||
setArrLoadingModules([]); | ||
|
||
let m = {...cityIOdata, "GEOGRID": lastJsonMessage.content.GEOGRID, "GEOGRIDDATA":lastJsonMessage.content.GEOGRIDDATA, tableName: tableName }; | ||
|
||
Object.keys(lastJsonMessage.content).forEach((key)=>{ | ||
if(possibleModules.includes(key) && key !== 'scenarios' && key !== 'indicators'){ | ||
m[key] = lastJsonMessage.content[key] | ||
} else if(key === 'deckgl'){ | ||
lastJsonMessage.content.deckgl | ||
.forEach((layer) => { | ||
m[layer.type]={ data: layer.data, properties: layer.properties } | ||
}); | ||
} | ||
} | ||
); | ||
// When we receive a GRID message, we ask for the scenarios of the table we´re | ||
// connected, and for the core modules | ||
sendJsonMessage({ | ||
type: "REQUEST_CORE_MODULES_LIST", | ||
content: {}, | ||
}) | ||
sendJsonMessage({ | ||
type: "LIST_SCENARIOS", | ||
content: {}, | ||
}) | ||
dispatch(updateCityIOdata(m)); | ||
verbose && | ||
console.log( | ||
"%c --- done updating from cityIO ---", | ||
"color: rgb(0, 255, 0)" | ||
); | ||
dispatch(toggleCityIOisDone(true)); | ||
} | ||
// if we have a new hash, start getting submodules | ||
getModules(); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [mainHash]); | ||
|
||
async function getModules() { | ||
// wait to get all of this table's hashes | ||
const newHashes = await getAPICall(cityioURL + "meta/hashes/"); | ||
// init array of GET promises | ||
const promises = []; | ||
// init array of modules names | ||
const loadingModulesArray = []; | ||
// get an array of modules to update | ||
const modulesToUpdate = cityIOSettings.cityIO.cityIOmodules.map( | ||
(x) => x.name | ||
); | ||
// for each of the modules in settings, add api call to promises | ||
modulesToUpdate.forEach((module) => { | ||
// If we receive a GEOGRIDDATA_UPDATE, the UI needs to refresh | ||
// the GEOGRIDDATA object | ||
else if (messageType === 'GEOGRIDDATA_UPDATE'){ | ||
verbose && console.log( | ||
` --- trying to update GEOGRIDDATA --- ${JSON.stringify(lastJsonMessage.content)}` | ||
); | ||
let m = {...cityIOdata, "GEOGRIDDATA":lastJsonMessage.content }; | ||
dispatch(updateCityIOdata(m)); | ||
verbose && | ||
console.log( | ||
"%c checking {" + module + "} for updates...", | ||
"color:rgb(200, 200, 0)" | ||
"%c --- done updating from cityIO ---", | ||
"color: rgb(0, 255, 0)" | ||
); | ||
dispatch(toggleCityIOisDone(true)); | ||
} | ||
|
||
//add this module name to array | ||
// of modules that we await for | ||
loadingModulesArray.push(module); | ||
|
||
// if this module has an old hash | ||
// we assume it is about to be updated | ||
|
||
if (hashes[module] !== newHashes[module]) { | ||
// add this module URL to an array of GET requests | ||
promises.push(getAPICall(`${cityioURL}${module}/`)); | ||
} else { | ||
promises.push(null); | ||
// If we receive a INDICATOR (MODULE) message, the UI needs to load | ||
// the module data | ||
// WIP | ||
else if (messageType === 'INDICATOR'){ | ||
verbose && console.log( | ||
` --- trying to update INDICATOR --- ${JSON.stringify(lastJsonMessage.content)}` | ||
); | ||
let m = {...cityIOdata} | ||
if('numeric' in lastJsonMessage.content.moduleData){ | ||
m = {...m, "indicators":lastJsonMessage.content.moduleData.numeric, tableName: tableName }; | ||
} | ||
if('heatmap' in lastJsonMessage.content.moduleData){ | ||
m = {...m, "heatmap":lastJsonMessage.content.moduleData.heatmap, tableName: tableName }; | ||
} | ||
setArrLoadingModules(loadingModulesArray); | ||
}); | ||
|
||
// GET all modules data | ||
const modulesFromCityIO = await Promise.all(promises); | ||
setHashes(newHashes); | ||
|
||
// update cityio object with modules data | ||
let modulesData = modulesToUpdate.reduce((obj, moduleName, index) => { | ||
// if this module has data | ||
if (modulesFromCityIO[index]) { | ||
verbose && | ||
console.log( | ||
"%c {" + | ||
moduleName + | ||
"} state has changed on cityIO. Getting new data...", | ||
"color: rgb(0, 200, 255)" | ||
); | ||
setArrLoadingModules(removeElement(arrLoadingModules, moduleName)); | ||
|
||
return { ...obj, [moduleName]: modulesFromCityIO[index] }; | ||
} else { | ||
return obj; | ||
if('deckgl' in lastJsonMessage.content.moduleData){ | ||
lastJsonMessage.content.moduleData.deckgl | ||
.forEach((layer) => { | ||
m[layer.type]={ data: layer.data, properties: layer.properties } | ||
}); | ||
} | ||
}, cityIOdata); | ||
let m = { ...modulesData, tableName: tableName }; | ||
dispatch(updateCityIOdata(m)); | ||
verbose && | ||
console.log( | ||
"%c --- done updating from cityIO ---", | ||
"color: rgb(0, 255, 0)" | ||
|
||
dispatch(updateCityIOdata(m)); | ||
verbose && | ||
console.log( | ||
"%c --- done updating from cityIO ---", | ||
"color: rgb(0, 255, 0)" | ||
); | ||
dispatch(toggleCityIOisDone(true)); | ||
} | ||
|
||
// If we receive a CORE_MODULES_LIST message, the UI loads | ||
// the available modules data | ||
else if (messageType === 'CORE_MODULES_LIST'){ | ||
verbose && console.log( | ||
` --- trying to update CORE_MODULES_LIST --- ${JSON.stringify(lastJsonMessage.content)}` | ||
); | ||
dispatch(toggleCityIOisDone(true)); | ||
} | ||
let m = {...cityIOdata, 'core_modules':lastJsonMessage.content } | ||
dispatch(updateCityIOdata(m)); | ||
verbose && | ||
console.log( | ||
"%c --- done updating from cityIO ---", | ||
"color: rgb(0, 255, 0)" | ||
); | ||
dispatch(toggleCityIOisDone(true)); | ||
} | ||
|
||
// If we receive a SCENARIOS message, the UI loads | ||
// the available scenarios | ||
else if (messageType === 'SCENARIOS'){ | ||
verbose && console.log( | ||
` --- trying to update SCENARIOS --- ${JSON.stringify(lastJsonMessage.content)}` | ||
); | ||
let m = {...cityIOdata, 'scenarios':lastJsonMessage.content } | ||
dispatch(updateCityIOdata(m)); | ||
verbose && | ||
console.log( | ||
"%c --- done updating from cityIO ---", | ||
"color: rgb(0, 255, 0)" | ||
); | ||
dispatch(toggleCityIOisDone(true)); | ||
} | ||
|
||
}, [lastJsonMessage]) | ||
|
||
return <LoadingProgressBar loadingModules={arrLoadingModules} />; | ||
|
||
}; | ||
|
||
export default CityIO; |
Oops, something went wrong.