Skip to content

Commit d266121

Browse files
authored
Merge pull request #196 from CityScope/feature/websockets
WIP: websockets
2 parents d0119a3 + 4ec6499 commit d266121

File tree

24 files changed

+1025
-197
lines changed

24 files changed

+1025
-197
lines changed

.env

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
REACT_APP_MAPBOX_TOKEN=pk.eyJ1IjoicmVsbm94IiwiYSI6ImNqd2VwOTNtYjExaHkzeXBzYm1xc3E3dzQifQ.X8r8nj4-baZXSsFgctQMsg
2-
PUBLIC_URL=https://cityscope.media.mit.edu/CS_cityscopeJS
2+
PUBLIC_URL=https://cityio.media.mit.edu
33
SKIP_PREFLIGHT_CHECK=true

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cityscopejs",
33
"repository": "https://github.com/CityScope/CS_cityscopeJS",
4-
"homepage": "https://cityscope.media.mit.edu/CS_cityscopeJS/",
4+
"homepage": "https://cityio.media.mit.edu",
55
"dependencies": {
66
"@emotion/react": "^11.10.4",
77
"@emotion/styled": "^11.10.4",
@@ -28,6 +28,7 @@
2828
"react-map-gl": "7.0.19",
2929
"react-redux": "^8.0.4",
3030
"react-scripts": "5.0.1",
31+
"react-use-websocket": "^4.5.0",
3132
"redux": "^4.2.0",
3233
"typescript": "^4.8.4"
3334
},

src/Components/CityIO/index.js

+152-129
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,183 @@
1+
/* eslint-disable react-hooks/exhaustive-deps */
12
import { useEffect, useState } from "react";
2-
import { cityIOSettings, generalSettings } from "../../settings/settings";
3+
import { cityIOSettings } from "../../settings/settings";
34
import {
45
updateCityIOdata,
56
toggleCityIOisDone,
67
} from "../../redux/reducers/cityIOdataSlice";
78
import { useSelector, useDispatch } from "react-redux";
8-
import { getAPICall } from "../../utils/utils";
9+
import useWebSocket, { ReadyState } from "react-use-websocket"
910
import LoadingProgressBar from "../LoadingProgressBar";
1011

11-
const removeElement = (array, elem) => {
12-
var index = array.indexOf(elem);
13-
if (index > -1) {
14-
array.splice(index, 1);
15-
}
16-
return array;
17-
};
18-
1912
const CityIO = (props) => {
13+
2014
const verbose = true; // set to true to see console logs
21-
const waitTimeMS = 5000;
2215
const dispatch = useDispatch();
2316
const cityIOdata = useSelector((state) => state.cityIOdataState.cityIOdata);
24-
const cityscopeProjectURL = generalSettings.csjsURL;
2517
const { tableName } = props;
26-
const [mainHash, setMainHash] = useState(null);
27-
const [hashes, setHashes] = useState({});
18+
const possibleModules = cityIOSettings.cityIO.cityIOmodules.map(module => module.name)
2819
const [arrLoadingModules, setArrLoadingModules] = useState([]);
29-
const cityioURL = `${cityIOSettings.cityIO.baseURL}table/${tableName}/`;
3020

31-
// test if cityIO is up and this table exists
21+
// Creation of the websocket connection. TODO: change WS_URL to env or property
22+
// sendJsonMessage: function that sends a message through the websocket channel
23+
// lastJsonMessage: object that contains the last message received through the websocket
24+
// readyState: indicates whether the WS is ready or not
25+
const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(
26+
cityIOSettings.cityIO.websocketURL,
27+
{
28+
share: true,
29+
shouldReconnect: () => true,
30+
},
31+
)
32+
33+
// When the WS connection state (readyState) changes to OPEN,
34+
// the UI sends a LISTEN (SUBSCRIBE) message to CityIO with the tableName prop
3235
useEffect(() => {
33-
const testCityIO = async () => {
34-
let test = await getAPICall(cityioURL + "meta/");
35-
if (test) {
36-
// start fetching API hashes to check for new data
37-
getCityIOmetaHash();
38-
verbose &&
39-
console.log(
40-
"%c cityIO is up, reading cityIO every " +
41-
cityIOSettings.cityIO.interval +
42-
"ms",
43-
"color: red"
44-
);
45-
} else {
46-
setArrLoadingModules([
47-
`cityIO might be down, please check { ${tableName} } is correct. Returning to cityScopeJS at ${cityscopeProjectURL} in ${
48-
waitTimeMS / 1000
49-
} seconds`,
50-
]);
51-
52-
new Promise((resolve) => {
53-
setTimeout(() => {
54-
window.location.assign(cityscopeProjectURL);
55-
}, waitTimeMS);
56-
resolve();
57-
});
58-
}
59-
};
60-
testCityIO();
61-
// eslint-disable-next-line react-hooks/exhaustive-deps
62-
}, [cityioURL]);
63-
64-
/**
65-
* gets the main hash of this cityIO table
66-
* on a constant loop to check for updates
67-
*/
68-
async function getCityIOmetaHash() {
69-
// recursively get hashes
70-
await getAPICall(cityioURL + "meta/id/").then(async (res) => {
71-
// is it a new hash?
72-
if (mainHash !== res) {
73-
setMainHash(res);
74-
}
75-
});
76-
// do it forever
77-
setTimeout(getCityIOmetaHash, cityIOSettings.cityIO.interval);
78-
}
36+
console.log("Connection state changed")
37+
if (readyState === ReadyState.OPEN) {
38+
sendJsonMessage({
39+
type: "LISTEN",
40+
content: {
41+
gridId: tableName,
42+
},
43+
})
44+
setArrLoadingModules([
45+
`Loading ${tableName} data.`,
46+
]);
47+
}
48+
}, [readyState])
49+
7950

51+
// When a new WebSocket message is received (lastJsonMessage) the UI checks
52+
// the type of the message and performs the suitable operation
8053
useEffect(() => {
81-
//! only update if hashId changes
82-
if (!mainHash) {
83-
return;
54+
55+
if(lastJsonMessage == null) return;
56+
console.log(`Got a new message: ${JSON.stringify(lastJsonMessage)}`)
57+
58+
let messageType = lastJsonMessage.type;
59+
60+
// If the message is of type GRID, the UI updates the GEOGRID and
61+
// GEOGRIDDATA, optionally, CityIO can send saved modules
62+
if (messageType === 'GRID'){
63+
verbose && console.log(
64+
` --- trying to update GEOGRID --- ${JSON.stringify(lastJsonMessage.content)}`
65+
);
66+
setArrLoadingModules([]);
67+
68+
let m = {...cityIOdata, "GEOGRID": lastJsonMessage.content.GEOGRID, "GEOGRIDDATA":lastJsonMessage.content.GEOGRIDDATA, tableName: tableName };
69+
70+
Object.keys(lastJsonMessage.content).forEach((key)=>{
71+
if(possibleModules.includes(key) && key !== 'scenarios' && key !== 'indicators'){
72+
m[key] = lastJsonMessage.content[key]
73+
} else if(key === 'deckgl'){
74+
lastJsonMessage.content.deckgl
75+
.forEach((layer) => {
76+
m[layer.type]={ data: layer.data, properties: layer.properties }
77+
});
78+
}
79+
}
80+
);
81+
// When we receive a GRID message, we ask for the scenarios of the table we´re
82+
// connected, and for the core modules
83+
sendJsonMessage({
84+
type: "REQUEST_CORE_MODULES_LIST",
85+
content: {},
86+
})
87+
sendJsonMessage({
88+
type: "LIST_SCENARIOS",
89+
content: {},
90+
})
91+
dispatch(updateCityIOdata(m));
92+
verbose &&
93+
console.log(
94+
"%c --- done updating from cityIO ---",
95+
"color: rgb(0, 255, 0)"
96+
);
97+
dispatch(toggleCityIOisDone(true));
8498
}
85-
// if we have a new hash, start getting submodules
86-
getModules();
87-
// eslint-disable-next-line react-hooks/exhaustive-deps
88-
}, [mainHash]);
89-
90-
async function getModules() {
91-
// wait to get all of this table's hashes
92-
const newHashes = await getAPICall(cityioURL + "meta/hashes/");
93-
// init array of GET promises
94-
const promises = [];
95-
// init array of modules names
96-
const loadingModulesArray = [];
97-
// get an array of modules to update
98-
const modulesToUpdate = cityIOSettings.cityIO.cityIOmodules.map(
99-
(x) => x.name
100-
);
101-
// for each of the modules in settings, add api call to promises
102-
modulesToUpdate.forEach((module) => {
99+
// If we receive a GEOGRIDDATA_UPDATE, the UI needs to refresh
100+
// the GEOGRIDDATA object
101+
else if (messageType === 'GEOGRIDDATA_UPDATE'){
102+
verbose && console.log(
103+
` --- trying to update GEOGRIDDATA --- ${JSON.stringify(lastJsonMessage.content)}`
104+
);
105+
let m = {...cityIOdata, "GEOGRIDDATA":lastJsonMessage.content };
106+
dispatch(updateCityIOdata(m));
103107
verbose &&
104108
console.log(
105-
"%c checking {" + module + "} for updates...",
106-
"color:rgb(200, 200, 0)"
109+
"%c --- done updating from cityIO ---",
110+
"color: rgb(0, 255, 0)"
107111
);
112+
dispatch(toggleCityIOisDone(true));
113+
}
108114

109-
//add this module name to array
110-
// of modules that we await for
111-
loadingModulesArray.push(module);
112-
113-
// if this module has an old hash
114-
// we assume it is about to be updated
115-
116-
if (hashes[module] !== newHashes[module]) {
117-
// add this module URL to an array of GET requests
118-
promises.push(getAPICall(`${cityioURL}${module}/`));
119-
} else {
120-
promises.push(null);
115+
// If we receive a INDICATOR (MODULE) message, the UI needs to load
116+
// the module data
117+
// WIP
118+
else if (messageType === 'INDICATOR'){
119+
verbose && console.log(
120+
` --- trying to update INDICATOR --- ${JSON.stringify(lastJsonMessage.content)}`
121+
);
122+
let m = {...cityIOdata}
123+
if('numeric' in lastJsonMessage.content.moduleData){
124+
m = {...m, "indicators":lastJsonMessage.content.moduleData.numeric, tableName: tableName };
125+
}
126+
if('heatmap' in lastJsonMessage.content.moduleData){
127+
m = {...m, "heatmap":lastJsonMessage.content.moduleData.heatmap, tableName: tableName };
121128
}
122-
setArrLoadingModules(loadingModulesArray);
123-
});
124-
125-
// GET all modules data
126-
const modulesFromCityIO = await Promise.all(promises);
127-
setHashes(newHashes);
128-
129-
// update cityio object with modules data
130-
let modulesData = modulesToUpdate.reduce((obj, moduleName, index) => {
131-
// if this module has data
132-
if (modulesFromCityIO[index]) {
133-
verbose &&
134-
console.log(
135-
"%c {" +
136-
moduleName +
137-
"} state has changed on cityIO. Getting new data...",
138-
"color: rgb(0, 200, 255)"
139-
);
140-
setArrLoadingModules(removeElement(arrLoadingModules, moduleName));
141-
142-
return { ...obj, [moduleName]: modulesFromCityIO[index] };
143-
} else {
144-
return obj;
129+
if('deckgl' in lastJsonMessage.content.moduleData){
130+
lastJsonMessage.content.moduleData.deckgl
131+
.forEach((layer) => {
132+
m[layer.type]={ data: layer.data, properties: layer.properties }
133+
});
145134
}
146-
}, cityIOdata);
147-
let m = { ...modulesData, tableName: tableName };
148-
dispatch(updateCityIOdata(m));
149-
verbose &&
150-
console.log(
151-
"%c --- done updating from cityIO ---",
152-
"color: rgb(0, 255, 0)"
135+
136+
dispatch(updateCityIOdata(m));
137+
verbose &&
138+
console.log(
139+
"%c --- done updating from cityIO ---",
140+
"color: rgb(0, 255, 0)"
141+
);
142+
dispatch(toggleCityIOisDone(true));
143+
}
144+
145+
// If we receive a CORE_MODULES_LIST message, the UI loads
146+
// the available modules data
147+
else if (messageType === 'CORE_MODULES_LIST'){
148+
verbose && console.log(
149+
` --- trying to update CORE_MODULES_LIST --- ${JSON.stringify(lastJsonMessage.content)}`
153150
);
154-
dispatch(toggleCityIOisDone(true));
155-
}
151+
let m = {...cityIOdata, 'core_modules':lastJsonMessage.content }
152+
dispatch(updateCityIOdata(m));
153+
verbose &&
154+
console.log(
155+
"%c --- done updating from cityIO ---",
156+
"color: rgb(0, 255, 0)"
157+
);
158+
dispatch(toggleCityIOisDone(true));
159+
}
160+
161+
// If we receive a SCENARIOS message, the UI loads
162+
// the available scenarios
163+
else if (messageType === 'SCENARIOS'){
164+
verbose && console.log(
165+
` --- trying to update SCENARIOS --- ${JSON.stringify(lastJsonMessage.content)}`
166+
);
167+
let m = {...cityIOdata, 'scenarios':lastJsonMessage.content }
168+
dispatch(updateCityIOdata(m));
169+
verbose &&
170+
console.log(
171+
"%c --- done updating from cityIO ---",
172+
"color: rgb(0, 255, 0)"
173+
);
174+
dispatch(toggleCityIOisDone(true));
175+
}
176+
177+
}, [lastJsonMessage])
156178

157179
return <LoadingProgressBar loadingModules={arrLoadingModules} />;
180+
158181
};
159182

160183
export default CityIO;

0 commit comments

Comments
 (0)