Skip to content

Commit

Permalink
Merge pull request #196 from CityScope/feature/websockets
Browse files Browse the repository at this point in the history
WIP: websockets
  • Loading branch information
RELNO authored Apr 21, 2024
2 parents d0119a3 + 4ec6499 commit d266121
Show file tree
Hide file tree
Showing 24 changed files with 1,025 additions and 197 deletions.
2 changes: 1 addition & 1 deletion .env
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "cityscopejs",
"repository": "https://github.com/CityScope/CS_cityscopeJS",
"homepage": "https://cityscope.media.mit.edu/CS_cityscopeJS/",
"homepage": "https://cityio.media.mit.edu",
"dependencies": {
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
Expand All @@ -28,6 +28,7 @@
"react-map-gl": "7.0.19",
"react-redux": "^8.0.4",
"react-scripts": "5.0.1",
"react-use-websocket": "^4.5.0",
"redux": "^4.2.0",
"typescript": "^4.8.4"
},
Expand Down
281 changes: 152 additions & 129 deletions src/Components/CityIO/index.js
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;
Loading

0 comments on commit d266121

Please sign in to comment.