diff --git a/.env b/.env index daf48846..e7994b16 100644 --- a/.env +++ b/.env @@ -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 diff --git a/package.json b/package.json index 1ee146db..c85ecf08 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" }, diff --git a/src/Components/CityIO/index.js b/src/Components/CityIO/index.js index 01ffc2f2..c993378b 100755 --- a/src/Components/CityIO/index.js +++ b/src/Components/CityIO/index.js @@ -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 ; + }; export default CityIO; diff --git a/src/settings/settings.js b/src/settings/settings.js index 0323843d..3c979b71 100644 --- a/src/settings/settings.js +++ b/src/settings/settings.js @@ -7,19 +7,32 @@ const getServerLocation = () => { const serverLocation = "cityio_local" in parsed - ? "http://127.0.0.1:5000/api/" - : "https://cityio.media.mit.edu/api/"; + ? "http://localhost:8080/api/" + : "https://cityio.media.mit.edu/cityio/api/"; console.log("cityIO server location: ", serverLocation); return serverLocation; }; + +const getWebsocketServerLocation = () => { + const location = window.location; + const parsed = queryString.parse(location.search); + + const serverLocation = + "cityio_local" in parsed + ? "ws://localhost:8080/interface" + : "wss://cityio.media.mit.edu/cityio/interface"; + console.log("cityIO websocket server location: ", serverLocation); + return serverLocation; +}; + // get the location of the app (local or remote) const getCSJSLocation = () => { const location = window.location; const parsed = queryString.parse(location.search); const cityscopejs_local_url = "cityscopejs_local" in parsed - ? "http://localhost:3000/CS_cityscopeJS/" - : "https://cityscope.media.mit.edu/CS_cityscopeJS/"; + ? "http://localhost:3000/" + : "https://cityio.media.mit.edu"; console.log("cityScopeJS location: ", cityscopejs_local_url); return cityscopejs_local_url; }; @@ -33,8 +46,10 @@ export const cityIOSettings = { "https://raw.githubusercontent.com/CityScope/CS_cityscopeJS/master/docs/", cityIO: { baseURL: getServerLocation(), + websocketURL: getWebsocketServerLocation(), - ListOfTables: "tables/list/", + ListOfTables: "table/list/", + headers: "table/headers/", interval: 500, cityIOmodules: [ { name: "header", expectUpdate: false }, @@ -126,6 +141,96 @@ export const expectedLayers = { initState: false, initSliderValue: 50, }, + ARC_LAYER_CHECKBOX: { + displayName: "Arc", + cityIOmoduleName: "arc", + initState: true, + initSliderValue: 50, + }, + COLUMN_LAYER_CHECKBOX: { + displayName: "Column", + cityIOmoduleName: "column", + initState: true, + initSliderValue: 50, + }, + CONTOUR_LAYER_CHECKBOX: { + displayName: "Contour", + cityIOmoduleName: "contours", + initState: true, + initSliderValue: 50, + }, + GEOJSON_BASE_LAYER_CHECKBOX: { + displayName: "GeoJSON", + cityIOmoduleName: "geojsonbase", + initState: true, + initSliderValue: 50, + }, + GRID_BASE_LAYER_CHECKBOX: { + displayName: "Grid", + cityIOmoduleName: "gridlayer", + initState: true, + initSliderValue: 50, + }, + GRIDCELL_LAYER_CHECKBOX: { + displayName: "Grid cell", + cityIOmoduleName: "gridcell", + initState: true, + initSliderValue: 50, + }, + HEATMAP_LAYER_CHECKBOX: { + displayName: "Heatmap", + cityIOmoduleName: "heatmap", + initState: true, + initSliderValue: 50, + }, + HEXAGON_LAYER_CHECKBOX: { + displayName: "Hexagon", + cityIOmoduleName: "hexagon", + initState: true, + initSliderValue: 50, + }, + ICON_LAYER_CHECKBOX: { + displayName: "Icon", + cityIOmoduleName: "icon", + initState: true, + initSliderValue: 100, + }, + LINE_LAYER_CHECKBOX: { + displayName: "Lines", + cityIOmoduleName: "lines", + initState: true, + initSliderValue: 100, + }, + PATH_LAYER_CHECKBOX: { + displayName: "Path", + cityIOmoduleName: "path", + initState: true, + initSliderValue: 75, + }, + SCATTER_LAYER_CHECKBOX: { + displayName: "Scatter", + cityIOmoduleName: "scatterplot", + initState: true, + initSliderValue: 75, + }, + SCENEGRAPH_LAYER_CHECKBOX: { + displayName: "Scene graph", + cityIOmoduleName: "scenegraph", + initState: true, + initSliderValue: 75, + }, + MESH_LAYER_CHECKBOX: { + displayName: "Mesh", + cityIOmoduleName: "simpleMesh", + initState: true, + initSliderValue: 50, + }, + TEXT_LAYER_CHECKBOX: { + displayName: "Text", + cityIOmoduleName: "textLayer", + initState: false, + initSliderValue: 100, + }, }; export const viewControlCheckboxes = { diff --git a/src/views/CityIOviewer/CityIOlist.js b/src/views/CityIOviewer/CityIOlist.js index d2a1d790..3c61e4d3 100644 --- a/src/views/CityIOviewer/CityIOlist.js +++ b/src/views/CityIOviewer/CityIOlist.js @@ -14,31 +14,19 @@ export default function CityIOlist(props) { }, [tablesList]); const fetchCityIOtables = async () => { - // ! https://stackoverflow.com/questions/37213783/waiting-for-all-promises-called-in-a-loop-to-finish const cityIOlistURL = - cityIOSettings.cityIO.baseURL + cityIOSettings.cityIO.ListOfTables; - // get all URLs - const tablesArr = await axios.get(cityIOlistURL); - // create array of all requests - const requestArr = tablesArr.data.map(async (tableName) => { - // const tableName = urlStr.split('/').pop() - const url = `${cityIOSettings.cityIO.baseURL}table/${tableName}/`; - return axios - .get(`${url}GEOGRID/properties/header/`) - .then((res) => - setTableList((oldArray) => [ - ...oldArray, - { tableURL: url, tableName: tableName, tableHeader: res.data }, - ]) - ) - .catch((error) => console.log(error.toString())); - }); - - Promise.all(requestArr).then(() => { - setIsLoading(false); - return tablesList; - }); - }; + cityIOSettings.cityIO.baseURL + cityIOSettings.cityIO.headers; + // get all table headers + let tablesArr = await axios.get(cityIOlistURL); + // create array of all headers + tablesArr = tablesArr.data.map(table => { + const url = `${cityIOSettings.cityIO.baseURL}table/${table.tableName}/`; + table = {...table, tableURL: url} + return table + }) + setTableList((oldArray) => [...tablesArr]); + setIsLoading(false); +}; useEffect(() => { fetchCityIOtables(); diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Arc.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Arc.js new file mode 100644 index 00000000..02391b63 --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Arc.js @@ -0,0 +1,35 @@ +import {ArcLayer} from '@deck.gl/layers'; + + /** + * Data format: + * [ + * { + * inbound: 72633, + * outbound: 74735, + * from: { + * name: '19th St. Oakland (19TH)', + * coordinates: [-122.269029, 37.80787] + * }, + * to: { + * name: '12th St. Oakland City Center (12TH)', + * coordinates: [-122.271604, 37.803664] + * }, + * ... + * ] + */ + export default function ArcBaseLayer({data, opacity}){ + if(data.arc){ + return new ArcLayer({ + id: 'arc-layer', + data: data.arc.data, + pickable: true, + getWidth: data.arc.properties.width || 12, + getSourcePosition: d => d.from.coordinates, + getTargetPosition: d => d.to.coordinates, + getSourceColor: d => d.sourceColor || [255, 140, 0], + getTargetColor: d => d.targetColor || [55, 140, 0], + opacity + }); + + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Column.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Column.js new file mode 100644 index 00000000..92f3ab1c --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Column.js @@ -0,0 +1,28 @@ +import {ColumnLayer} from '@deck.gl/layers'; + + /** + * Data format: + * [ + * {centroid: [-122.4, 37.7], value: 0.2}, + * ... + * ] + */ + export default function ColumnBaseLayer({data, opacity}){ + if(data.column){ + return new ColumnLayer({ + id: 'column-layer', + data: data.column.data, + diskResolution: data.column.properties.resolution || 12, + radius: data.column.properties.radius || 30, + extruded: data.column.properties.extruded || true, + pickable: data.column.properties.pickable || true, + elevationScale: data.column.properties.elevationScale || 1, + getPosition: d => d.centroid, + getFillColor: d => [48, 128, d.value * 255, 255], + getLineColor: [0, 0, 0], + getElevation: d => d.value, + opacity + }); + + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Contour.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Contour.js new file mode 100644 index 00000000..92582d93 --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Contour.js @@ -0,0 +1,21 @@ +import {ContourLayer} from '@deck.gl/aggregation-layers'; + +/** + * Data format: + * [ + * {coordinates: [-122.42177834, 37.78346622]}, + * ... + * ] + */ +export default function ContourBaseLayer({data, opacity}){ + if(data.contours){ + return new ContourLayer({ + id: 'contourLayer', + contours: data.contours.data, + cellSize: data.contours.properties.cellSize || 200, + getPosition: d => d.coordinates, + opacity + }); + + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/GeoJson.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/GeoJson.js new file mode 100644 index 00000000..811359c7 --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/GeoJson.js @@ -0,0 +1,29 @@ +import {GeoJsonLayer} from '@deck.gl/layers'; + + /** + * Data format: + * Valid GeoJSON object + */ + export default function GeoJsonBaseLayer({data, opacity}){ + if(data.geojsonbase){ + return new GeoJsonLayer({ + id: 'geojson-layer', + data: data.geojsonbase.data, + pickable: data.geojsonbase.properties.pickable || true, + stroked: data.geojsonbase.properties.stroked || false, + filled: data.geojsonbase.properties.filled || true, + extruded: data.geojsonbase.properties.extruded || true, + pointType: data.geojsonbase.properties.pointType || 'circle', + lineWidthScale: data.geojsonbase.properties.lineWidthScale || 20, + lineWidthMinPixels: data.geojsonbase.properties.lineWidthMinPixels || 2, + getFillColor: [160, 160, 180, 200], + getLineColor: d => d.properties.color, + getPointRadius: data.geojsonbase.properties.pointRadius || 100, + getLineWidth: data.geojsonbase.properties.lineWidth || 1, + getElevation: data.geojsonbase.properties.elevation || 30, + opacity + }); + + + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Grid.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Grid.js new file mode 100644 index 00000000..30893ede --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Grid.js @@ -0,0 +1,24 @@ +import {GridLayer} from '@deck.gl/aggregation-layers'; + + /** + * Data format: + * [ + * {coordinates: [-122.42177834, 37.78346622]}, + * ... + * ] + */ + export default function GridBaseLayer({data, opacity}){ + if(data.gridlayer){ + return new GridLayer({ + id: 'new-grid-layer', + data: data.gridlayer.data, + pickable: data.gridlayer.properties.pickable || true, + extruded: data.gridlayer.properties.extruded || true, + cellSize: data.gridlayer.properties.cellSize || 200, + elevationScale: data.gridlayer.properties.elevationScale || 4, + getPosition: d => d.coordinates, + opacity + }); + + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/GridCell.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/GridCell.js new file mode 100644 index 00000000..f465e59d --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/GridCell.js @@ -0,0 +1,26 @@ +import {GridCellLayer} from '@deck.gl/layers'; + + /** + * Data format: + * [ + * {centroid: [-122.4, 37.7], + * value: 100}, + * ... + * ] + */ + export default function GridCellBaseLayer({data, opacity}){ + if(data.gridcell){ + return new GridCellLayer({ + id: 'grid-cell-layer', + data: data.gridcell.data, + pickable: data.gridcell.properties.pickable || true, + extruded: data.gridcell.properties.extruded || true, + cellSize: data.gridcell.properties.cellSize || 200, + elevationScale: data.gridcell.properties.elevationScale || 5000, + getPosition: d => d.centroid, + getFillColor: d => [48, 128, d.value * 255, 255], + getElevation: d => d.value, + opacity + }); + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Heatmap.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Heatmap.js new file mode 100644 index 00000000..1e8afc59 --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Heatmap.js @@ -0,0 +1,23 @@ +import {HeatmapLayer} from '@deck.gl/aggregation-layers'; + + /** + * Data format: + * [ + * {coordinates: [-122.42177834, 37.78346622], weight: 10}, + * ... + * ] + */ + export default function HeatmapBaseLayer({data, opacity}){ + + if(data.heatmap){ + + return new HeatmapLayer({ + id: 'heatmapLayer', + data: data.heatmap.data, + getPosition: d => d.coordinates, + getWeight: d => d.weight, + aggregation: 'SUM', + opacity + }); + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Hexagon.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Hexagon.js new file mode 100644 index 00000000..75660aa8 --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Hexagon.js @@ -0,0 +1,23 @@ +import {HexagonLayer} from '@deck.gl/aggregation-layers'; + + /** + * Data format: + * [ + * {coordinates: [-122.42177834, 37.78346622]}, + * ... + * ] + */ + export default function HexagonBaseLayer({data, opacity}){ + if(data.hexagon){ + return new HexagonLayer({ + id: 'hexagon-layer', + data: data.hexagon.data, + pickable: data.hexagon.properties.pickable || true, + extruded: data.hexagon.properties.extruded || true, + radius: data.hexagon.properties.radius || 200, + elevationScale: data.hexagon.properties.elevationScale || 4, + getPosition: d => d.coordinates, + opacity + }); + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Icon.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Icon.js new file mode 100644 index 00000000..2f7d26ac --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Icon.js @@ -0,0 +1,30 @@ +import {IconLayer} from '@deck.gl/layers'; + + /** + * Data format: + * [ + * {coordinates: [-122.466233, 37.684638]}, + * ... + * ] + */ + export default function IconBaseLayer({data, opacity}){ + if(data.icon){ + return new IconLayer({ + id: 'icon-layer', + data: data.icon.data, + pickable: data.icon.properties.pickable || true, + // iconAtlas and iconMapping are required + // getIcon: return a string + getIcon: d => ({ + url: d.icon, + width: d.width || 128, + height: d.height || 128, + anchorY: d.anchorY || 128 + }), + sizeScale: data.icon.properties.sizeScale || 10, + sizeMaxPixels: data.icon.properties.sizeMaxPixels || 10, + getPosition: d => [d.coordinates[0],d.coordinates[1],d.elevation || 30], + opacity + }); + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Line.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Line.js new file mode 100644 index 00000000..4c382376 --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Line.js @@ -0,0 +1,33 @@ +import {LineLayer} from '@deck.gl/layers'; + + /** + * Data format: + * [ + * { + * inbound: 72633, + * outbound: 74735, + * from: { + * name: '19th St. Oakland (19TH)', + * coordinates: [-122.269029, 37.80787] + * }, + * to: { + * name: '12th St. Oakland City Center (12TH)', + * coordinates: [-122.271604, 37.803664] + * }, + * ... + * ] + */ + export default function LineBaseLayer({data, opacity}){ + if(data.lines){ + return new LineLayer({ + id: 'line-layer', + data: data.lines.data, + pickable: data.lines.properties.pickable || true, + getWidth: data.lines.properties.width || 50, + getSourcePosition: d => d.from.coordinates, + getTargetPosition: d => d.to.coordinates, + getColor: d => d.color || [200, 140, 0], + opacity + }); + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Path.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Path.js new file mode 100644 index 00000000..93350930 --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Path.js @@ -0,0 +1,29 @@ +import {PathLayer} from '@deck.gl/layers'; + + /** + * Data format: + * [ + * { + * path: [[-122.4, 37.7], [-122.5, 37.8], [-122.6, 37.85]], + * name: 'Richmond - Millbrae', + * color: [255, 0, 0] + * }, + * ... + * ] + */ + export default function PathBaseLayer({data, opacity}){ + if(data.path){ + return new PathLayer({ + id: 'path-layer', + data: data.path.data, + pickable: data.path.properties.pickable || true, + widthScale: data.path.properties.widthScale || 20, + widthMinPixels: data.path.properties.widthMinPixels || 2, + getPath: d => d.path, + getColor: d => d.color || [255, 0, 0], + getWidth: d => d.width || 5, + opacity + }); + + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Scatterplot.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Scatterplot.js new file mode 100644 index 00000000..95d476d6 --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Scatterplot.js @@ -0,0 +1,31 @@ +import {ScatterplotLayer} from '@deck.gl/layers'; + + /** + * Data format: + * [ + * {name: 'Colma (COLM)', address: '365 D Street, Colma CA 94014', exits: 4214, coordinates: [-122.466233, 37.684638]}, + * ... + * ] + */ + export default function ScatterplotBaseLayer({data, opacity}){ + if(data.scatterplot){ + return new ScatterplotLayer({ + id: 'scatterplot-layer', + data: data.scatterplot.data, + pickable: data.path.properties.pickable || true, + stroked: data.path.properties.stroked || true, + filled: data.path.properties.filled || true, + radiusScale: data.path.properties.radiusScale || 6, + radiusMinPixels: data.path.properties.radiusMinPixels || 1, + radiusMaxPixels: data.path.properties.radiusMaxPixels || 100, + lineWidthMinPixels: data.path.properties.lineWidthMinPixels || 1, + getPosition: d => d.coordinates, + getRadius: d => Math.sqrt(d.exits), + getFillColor: d => [255, 140, 0], + getLineColor: d => [0, 0, 0], + opacity + }); + + + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Scenegraph.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Scenegraph.js new file mode 100644 index 00000000..e5b125a8 --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/Scenegraph.js @@ -0,0 +1,30 @@ +import {ScenegraphLayer} from '@deck.gl/mesh-layers'; + + /** + * Data format: + * [ + * {name: 'Colma (COLM)', code:'CM', address: '365 D Street, Colma CA 94014', exits: 4214, coordinates: [-122.466233, 37.684638]}, + * ... + * ] + */ + export default function ScenegraphBaseLayer({data, opacity}){ + if(data.scenegraph){ + return new ScenegraphLayer({ + id: 'scenegraph-layer', + data: data.scenegraph.data, + pickable: data.scenegraph.properties.pickable || true, + scenegraph: data.scenegraph.properties.scenegraph || 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/BoxAnimated/glTF-Binary/BoxAnimated.glb', + getPosition: d => d.coordinates, + getOrientation: d => [0, Math.random() * 180, 90], + _animations: { + '*': {speed: 5} + }, + sizeScale: data.scenegraph.properties.sizeScale || 500, + _lighting: 'pbr', + opacity + }); + + + + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/SimpleMesh.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/SimpleMesh.js new file mode 100644 index 00000000..087b257a --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/SimpleMesh.js @@ -0,0 +1,34 @@ +import {SimpleMeshLayer} from '@deck.gl/mesh-layers'; +import {OBJLoader} from "@loaders.gl/obj"; + + /** + * Data format: + * [ + * { + * position: [-122.45, 37.7], + * angle: 0, + * color: [255, 0, 0] + * }, + * { + * position: [-122.46, 37.73], + * angle: 90, + * color: [0, 255, 0] + * }, + * ... + * ] + */ + export default function SimpleMeshBaseLayer({data, opacity}){ + if(data.simpleMesh){ + return new SimpleMeshLayer({ + id: 'mesh-layer', + data: data.simpleMesh.data, + mesh: data.simpleMesh.properties.mesh || 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/humanoid_quad.obj', + getPosition: d => [d.position[0],d.position[1],0], + getColor: d => d.color, + getOrientation: d => [0, d.angle, 0], + loaders:[OBJLoader], + sizeScale: data.simpleMesh.properties.sizeScale || 1, + opacity + }); + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/base/TextLayer.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/TextLayer.js new file mode 100644 index 00000000..4cf60192 --- /dev/null +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/base/TextLayer.js @@ -0,0 +1,31 @@ +import {TextLayer} from '@deck.gl/layers'; +/** + * Data format: + * [ + * {name: 'Colma (COLM)', address: '365 D Street, Colma CA 94014', coordinates: [-122.466233, 37.684638]}, + * ... + * ] + */ + export default function TextBaseLayer({data, opacity}){ + if(data.textLayer){ + return new TextLayer({ + id: 'text-layer', + data: data.textLayer.data, + pickable: data.textLayer.properties.pickable || true, + getPosition: d => [d.coordinates[0],d.coordinates[1],20], + getText: d => d.text, + getAngle: 0, + getTextAnchor: 'middle', + getAlignmentBaseline: 'center', + background: true, + backgroundPadding: [3,3,3,3], + getBackgroundColor: [255, 255, 255, 200], + getBorderWidth: 1, + billboard: true, + getSize: 5, + sizeScale: 2, + opacity + }); + + } + } diff --git a/src/views/CityScopeJS/DeckglMap/deckglLayers/index.js b/src/views/CityScopeJS/DeckglMap/deckglLayers/index.js index 393e0d85..2a67a92d 100644 --- a/src/views/CityScopeJS/DeckglMap/deckglLayers/index.js +++ b/src/views/CityScopeJS/DeckglMap/deckglLayers/index.js @@ -6,4 +6,18 @@ export { default as TextualLayer } from "./TextualLayer"; export { default as GeojsonLayer } from "./GeojsonLayer"; export { default as TileMapLayer } from "./TileMapLayer"; export { default as TrafficLayer } from "./TrafficLayer"; - +export {default as ArcBaseLayer} from "./base/Arc" +export {default as ColumnBaseLayer} from "./base/Column" +export {default as ContourBaseLayer} from "./base/Contour" +export {default as GeoJsonBaseLayer} from "./base/GeoJson" +export {default as GridBaseLayer} from "./base/Grid" +export {default as GridCellBaseLayer} from "./base/GridCell" +export {default as HeatmapBaseLayer} from "./base/Heatmap" +export {default as HexagonBaseLayer} from "./base/Hexagon" +export {default as IconBaseLayer} from "./base/Icon" +export {default as LineBaseLayer} from "./base/Line" +export {default as PathBaseLayer} from "./base/Path" +export {default as ScatterplotBaseLayer} from "./base/Scatterplot" +export {default as ScenegraphBaseLayer} from "./base/Scenegraph" +export {default as SimpleMeshBaseLayer} from "./base/SimpleMesh" +export {default as TextBaseLayer} from "./base/TextLayer" diff --git a/src/views/CityScopeJS/DeckglMap/index.js b/src/views/CityScopeJS/DeckglMap/index.js index 0bf02e97..80bd58b4 100644 --- a/src/views/CityScopeJS/DeckglMap/index.js +++ b/src/views/CityScopeJS/DeckglMap/index.js @@ -2,7 +2,6 @@ import { useState, useEffect } from "react"; import { useSelector } from "react-redux"; import PaintBrush from "../../../Components/PaintBrush"; import { LayerHoveredTooltip } from "../../../Components/LayerHoveredTooltip"; -import { postToCityIO } from "../../../utils/utils"; import DeckglBase from "./DeckglBase"; import "mapbox-gl/dist/mapbox-gl.css"; import { @@ -14,8 +13,25 @@ import { GeojsonLayer, TileMapLayer, TrafficLayer, + ArcBaseLayer, + ColumnBaseLayer, + ContourBaseLayer, + GeoJsonBaseLayer, + GridBaseLayer, + GridCellBaseLayer, + HeatmapBaseLayer, + HexagonBaseLayer, + IconBaseLayer, + LineBaseLayer, + PathBaseLayer, + ScatterplotBaseLayer, + ScenegraphBaseLayer, + SimpleMeshBaseLayer, + TextBaseLayer } from "./deckglLayers"; import { processGridData } from "./deckglLayers/GridLayer"; +import useWebSocket from "react-use-websocket" +import { cityIOSettings } from "../../../settings/settings"; export default function DeckGLMap() { // get cityio data from redux store @@ -41,23 +57,37 @@ export default function DeckGLMap() { const toggleRotateCamera = menuState?.viewSettingsMenuState?.ROTATE_CHECKBOX; - // update the grid layer with every change to GEOGRIDDATA - useEffect(() => { - setGEOGRIDDATA(processGridData(cityIOdata)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [cityIOdata.GEOGRIDDATA]); + const { sendJsonMessage } = useWebSocket( + cityIOSettings.cityIO.websocketURL, + { + share: true, + shouldReconnect: () => true, + }, + ) - // post GEOGRIDDATA changes to cityIO + // Send changes to cityIO useEffect(() => { if (!editModeToggle && GEOGRIDDATA) { let dataProps = []; for (let i = 0; i < GEOGRIDDATA.features.length; i++) { dataProps[i] = GEOGRIDDATA.features[i].properties; } - postToCityIO(dataProps, cityIOdata.tableName, "/GEOGRIDDATA/"); + sendJsonMessage({ + type: "UPDATE_GRID", + content: { + geogriddata: dataProps, + }, + }); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editModeToggle]); + }, [editModeToggle]) + + // update the grid layer with every change to GEOGRIDDATA + useEffect(() => { + setGEOGRIDDATA(processGridData(cityIOdata)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [cityIOdata.GEOGRIDDATA]); + const layersKey = { TILE_MAP: TileMapLayer(), @@ -146,9 +176,143 @@ export default function DeckGLMap() { layersMenu.TRAFFIC_LAYER_CHECKBOX && layersMenu.TRAFFIC_LAYER_CHECKBOX.slider * 0.01, }), + + ARC: ArcBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.ARC_LAYER_CHECKBOX && + layersMenu.ARC_LAYER_CHECKBOX.slider * 0.01, + }), + + COLUMN: ColumnBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.COLUMN_LAYER_CHECKBOX && + layersMenu.COLUMN_LAYER_CHECKBOX.slider * 0.01, + }), + + CONTOUR: ContourBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.CONTOUR_LAYER_CHECKBOX && + layersMenu.CONTOUR_LAYER_CHECKBOX.slider * 0.01, + }), + + GEOJSON_BASE: GeoJsonBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.GEOJSON_BASE_LAYER_CHECKBOX && + layersMenu.GEOJSON_BASE_LAYER_CHECKBOX.slider * 0.01, + }), + + GRID_BASE: GridBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.GRID_BASE_LAYER_CHECKBOX && + layersMenu.GRID_BASE_LAYER_CHECKBOX.slider * 0.01, + }), + + GRID_CELL: GridCellBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.GRIDCELL_LAYER_CHECKBOX && + layersMenu.GRIDCELL_LAYER_CHECKBOX.slider * 0.01, + }), + + HEATMAP: HeatmapBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.HEATMAP_LAYER_CHECKBOX && + layersMenu.HEATMAP_LAYER_CHECKBOX.slider * 0.01, + }), + + HEXAGON: HexagonBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.HEXAGON_LAYER_CHECKBOX && + layersMenu.HEXAGON_LAYER_CHECKBOX.slider * 0.01, + }), + + ICON: IconBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.ICON_LAYER_CHECKBOX && + layersMenu.ICON_LAYER_CHECKBOX.slider * 0.01, + }), + + LINE: LineBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.LINE_LAYER_CHECKBOX && + layersMenu.LINE_LAYER_CHECKBOX.slider * 0.01, + }), + + PATH: PathBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.PATH_LAYER_CHECKBOX && + layersMenu.PATH_LAYER_CHECKBOX.slider * 0.01, + }), + + SCATTERPLOT: ScatterplotBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.SCATTER_LAYER_CHECKBOX && + layersMenu.SCATTER_LAYER_CHECKBOX.slider * 0.01, + }), + + SCENEGRAPH: ScenegraphBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.SCENEGRAPH_LAYER_CHECKBOX && + layersMenu.SCENEGRAPH_LAYER_CHECKBOX.slider * 0.01, + }), + + MESH: SimpleMeshBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.MESH_LAYER_CHECKBOX && + layersMenu.MESH_LAYER_CHECKBOX.slider * 0.01, + }), + + TEXT: TextBaseLayer({ + data: cityIOdata, + opacity: + layersMenu && + layersMenu.TEXT_LAYER_CHECKBOX && + layersMenu.TEXT_LAYER_CHECKBOX.slider * 0.01, + }), + }; const layerOrder = [ + "TEXT", + "ICON", + "MESH", + "SCENEGRAPH", + "SCATTERPLOT", + "PATH", + "LINE", + "GRID_CELL", + "GRID_BASE", + "GEOJSON_BASE", + "CONTOUR", + "COLUMN", + "ARC", "ABM", "AGGREGATED_TRIPS", "TILE_MAP", @@ -157,6 +321,8 @@ export default function DeckGLMap() { "GEOJSON", "ACCESS", "TRAFFIC", + "HEATMAP", + "HEXAGON" ]; const renderDeckLayers = () => { diff --git a/src/views/CityScopeJS/MenuContainer/ScenariosMenu/index.js b/src/views/CityScopeJS/MenuContainer/ScenariosMenu/index.js index f8924afe..c86010d0 100644 --- a/src/views/CityScopeJS/MenuContainer/ScenariosMenu/index.js +++ b/src/views/CityScopeJS/MenuContainer/ScenariosMenu/index.js @@ -15,44 +15,54 @@ import { Badge, TextField, } from "@mui/material"; +import DeleteSweepOutlinedIcon from '@mui/icons-material/DeleteSweepOutlined'; import DeleteIcon from "@mui/icons-material/Delete"; -import { postToCityIO, getModule, getTableID } from "../../../../utils/utils"; +import RestorePageOutlinedIcon from '@mui/icons-material/RestorePageOutlined'; +import useWebSocket from "react-use-websocket" +import { cityIOSettings } from "../../../../settings/settings"; export default function ScenariosMenu() { const [scenariosButtonsList, setScenariosButtonsList] = useState([]); + const [scenariosBinButtonsList, setScenariosBinButtonsList] = useState([]); const [scenarioToRestore, setScenariosToRestore] = useState(); const [saveDialogState, setSaveDialogState] = useState(false); const [loadDialogState, setLoadDialogState] = useState(false); + const [binDialogState, setBinDialogState] = useState(false); const [scenarioTextInput, setScenarioTextInput] = useState({ name: "", description: "", }); // get cityIO data from redux store const cityIOdata = useSelector((state) => state.cityIOdataState.cityIOdata); - // get cityio name from redux store - const cityIOtableName = useSelector( - (state) => state.cityIOdataState.cityIOtableName - ); + + const { sendJsonMessage } = useWebSocket( + cityIOSettings.cityIO.websocketURL, + { + share: true, + shouldReconnect: () => true, + }, + ) const handleSaveThisState = () => { handleClose(); - getTableID(cityIOtableName).then((id) => { const newScenario = { // ! to be updated from dynamic ui element - name: scenarioTextInput.name || `${id}`, - hash: id, + name: scenarioTextInput.name || `noname`, description: - scenarioTextInput.description || `no description for ${id} yet.`, - }; - const tempArr = cityIOdata.scenarios ? [...cityIOdata.scenarios] : []; - tempArr.push(newScenario); - postToCityIO(tempArr, cityIOtableName, `/scenarios/`); - }); + scenarioTextInput.description || `no description yet.`, + } + sendJsonMessage({ + type: "SAVE_SCENARIO", + content: newScenario, + }) + + }; const handleClose = () => { setLoadDialogState(false); setSaveDialogState(false); + setBinDialogState(false); }; const handleOpenDialog = (scenario) => { @@ -64,13 +74,11 @@ export default function ScenariosMenu() { const handleRestoreThisState = async () => { if (!scenarioToRestore) return; - await getModule(scenarioToRestore.hash) - .then((module) => { - postToCityIO(module, cityIOtableName, `/GEOGRIDDATA/`); - }) - .finally(() => { - handleClose(); - }); + sendJsonMessage({ + type: "RESTORE_SCENARIO", + content: {name: scenarioToRestore.name}, + }) + handleClose(); }; const handleDeleteThisState = (scenario) => { @@ -84,14 +92,37 @@ export default function ScenariosMenu() { var index = tempArr.indexOf(scnToDelete[0]); if (index !== -1) { // remove the scenario from the array - tempArr.splice(index, 1); + let scenarioToMod = tempArr[index] + sendJsonMessage({ + type: "MODIFY_SCENARIO", + content: {name: scenarioToMod.name, isInBin:true}, + }) + } + handleClose() + }; + + const handleRestoreSce = (scenario) => { + // copy the scenarios array + const tempArr = [...cityIOdata.scenarios]; + // find the clicked scenario in the array + var scnToDelete = tempArr.filter((obj) => { + return obj.hash === scenario.hash; + }); + // find the index of the scenario to delete + var index = tempArr.indexOf(scnToDelete[0]); + if (index !== -1) { + // remove the scenario from the array + let scenarioToMod = tempArr[index] + sendJsonMessage({ + type: "MODIFY_SCENARIO", + content: {name: scenarioToMod.name, isInBin:false}, + }) } - // post the new array to the server - postToCityIO(tempArr, cityIOtableName, `/scenarios/`); + handleClose() }; const createScenariosButtons = () => { - const scenariosButtons = cityIOdata.scenarios.map((scenario, i) => { + const scenariosButtons = cityIOdata.scenarios.filter(x => !x.isInBin).map((scenario, i) => { return ( { + const scenariosButtons = cityIOdata.scenarios.filter(x => x.isInBin).map((scenario, i) => { + return ( + + + + + + { + handleRestoreSce(scenario); + }} + aria-label="delete" + size="large" + > + + + + ); + }); + return scenariosButtons; + }; + useEffect(() => { // check if there are any scenarios in the cityIOdata if (!cityIOdata.scenarios) return; const scenariosButtons = createScenariosButtons(); setScenariosButtonsList(scenariosButtons); + const scenariosBinButtons = createBinScenariosButtons(); + setScenariosBinButtonsList(scenariosBinButtons); // eslint-disable-next-line react-hooks/exhaustive-deps }, [cityIOdata]); @@ -170,6 +257,7 @@ export default function ScenariosMenu() { > Save This Scenario + {scenariosButtonsList} @@ -237,6 +325,22 @@ export default function ScenariosMenu() { + + + + {"Bin"} + + + The scenarios in the bin will be permanently deleted after 15 days. + + + {scenariosBinButtonsList} + + + + + + ); } diff --git a/src/views/GridEditor/EditorMenu/CommitGridMenu/index.js b/src/views/GridEditor/EditorMenu/CommitGridMenu/index.js index defc59ae..c06c99e2 100644 --- a/src/views/GridEditor/EditorMenu/CommitGridMenu/index.js +++ b/src/views/GridEditor/EditorMenu/CommitGridMenu/index.js @@ -10,7 +10,7 @@ import LoadingButton from "@mui/lab/LoadingButton"; const reqResponseUI = (response, tableName) => { let cityscopeJSendpoint = - "https://cityscope.media.mit.edu/CS_cityscopeJS/?cityscope=" + tableName; + "https://cityio.media.mit.edu/?cityscope=" + tableName; // create the feedback text let resText = (