diff --git a/packages/simulator/web/routes.js b/packages/simulator/web/routes.js index cad67ebd..bb62bc54 100644 --- a/packages/simulator/web/routes.js +++ b/packages/simulator/web/routes.js @@ -34,6 +34,7 @@ function start(socket) { socket.data.experiment = experiment experiment.subscriptions = subscribe(experiment, socket) experiment.virtualTime.waitUntil(moment().endOf('day').valueOf()).then(() => { + socket.emit('reset') info('Experiment finished. Restarting...') process.kill(process.pid, 'SIGUSR2') }) @@ -58,7 +59,7 @@ function register(io) { socket.data.emitTaxiUpdates = defaultEmitters.includes('taxis') socket.data.emitBusUpdates = defaultEmitters.includes('buses') - socket.emit('reset') + socket.emit('init') socket.on('reset', () => { socket.data.experiment.subscriptions.map((e) => e.unsubscribe()) start(socket) @@ -70,7 +71,7 @@ function register(io) { socket.on('experimentParameters', (value) => { info('New expiriment settings: ', value) save(value) - socket.emit('reset') + socket.emit('init') }) socket.emit('parameters', socket.data.experiment.parameters) diff --git a/packages/visualisation/.dockerignore b/packages/visualisation/.dockerignore index cbfd549d..d8cbbeda 100644 --- a/packages/visualisation/.dockerignore +++ b/packages/visualisation/.dockerignore @@ -1,5 +1,7 @@ -dist/ .dockerignore +.gitignore + +dist/ Dockerfile node_modules README.md diff --git a/packages/visualisation/src/App.jsx b/packages/visualisation/src/App.jsx index 4130a5d9..a62d6f25 100644 --- a/packages/visualisation/src/App.jsx +++ b/packages/visualisation/src/App.jsx @@ -1,19 +1,14 @@ -import React, { useState, useEffect } from 'react' +import React, { useState } from 'react' +import 'jsoneditor-react/es/editor.min.css' import { useSocket } from './hooks/useSocket.js' + import Map from './Map.jsx' -import PlaybackOptions from './components/PlaybackOptions' import Loading from './components/Loading' -import styled from 'styled-components' -import ResetIcon from './icons/svg/resetIcon.svg' -import TransparentButton from './components/TransparentButton' -import SideMenu from './components/SideMenu' - -const Wrapper = styled.div` - position: absolute; - z-index: 2; - bottom: 3rem; - left: 11.3rem; -` +import PlaybackOptions from './components/PlaybackOptions' +import ResetExperiment from './components/ResetExperiment' +import EditExperimentModal from './components/EditExperimentModal' +import Logo from './components/Logo' +import ExperimentDoneModal from './components/ExperimentDoneModal/index.jsx' const App = () => { const [activeCar, setActiveCar] = useState(null) @@ -30,9 +25,12 @@ const App = () => { const [commercialAreasLayer, setCommercialAreasLayer] = useState(false) const [busLineLayer, setBusLineLayer] = useState(true) const [kommunLayer, setKommunLayer] = useState(true) - const [newParameters, setNewParameters] = useState({}) + const [experimentParameters, setExperimentParameters] = useState({}) const [currentParameters, setCurrentParameters] = useState({}) const [fleets, setFleets] = useState({}) + const [showEditExperimentModal, setShowEditExperimentModal] = useState(false) + const [showExperimentDoneModal, setShowExperimentDoneModal] = useState(false) + const [previousExperimentId, setPreviousExperimentId] = useState(null) const [connected, setConnected] = useState(false) @@ -61,12 +59,13 @@ const App = () => { setBusLineLayer, } - const newExperiment = () => { - socket.emit('experimentParameters', newParameters) + const restartSimulation = () => { + setShowEditExperimentModal(false) + socket.emit('experimentParameters', experimentParameters) } - useSocket('reset', () => { - console.log('received reset') + useSocket('init', () => { + console.log('Init experiment') setBookings([]) setPassengers([]) setCars([]) @@ -78,6 +77,12 @@ const App = () => { socket.emit('speed', speed) // reset speed on server }) + useSocket('reset', () => { + console.log('Reset experiment') + setPreviousExperimentId(experimentParameters.id) + setShowExperimentDoneModal(true) + }) + function upsert(array, object, idProperty = 'id', deep = false) { const currentIndex = array.findIndex( (k) => k[idProperty] === object[idProperty] @@ -183,7 +188,11 @@ const App = () => { }) useSocket('parameters', (currentParameters) => { - console.log('new experimentId', currentParameters.id) + console.log('ExperimentId', currentParameters.id) + + if (!previousExperimentId) { + setPreviousExperimentId(currentParameters.id) + } setCurrentParameters(currentParameters) const layerSetFunctions = { @@ -206,10 +215,8 @@ const App = () => { } }) - console.log(currentParameters) setFleets(currentParameters.fleets) - - setNewParameters(currentParameters) + setExperimentParameters(currentParameters) }) const [passengers, setPassengers] = React.useState([]) useSocket('passengers', (passengers) => { @@ -257,27 +264,18 @@ const App = () => { setConnected(true) }) + /** + * Update the fleets part of the parameters. + */ + const saveFleets = (updatedJson) => { + setExperimentParameters({ ...experimentParameters, fleets: updatedJson }) + } + return ( <> - - resetSimulation()}> - Reset - - - + - + {/* Loader. */} {(!connected || reset || !cars.length || !bookings.length) && ( { parameters={currentParameters} /> )} + + {/* Playback controls. */} + + + {/* Reset experiment button. */} + + + {/* Edit experiment modal. */} + + + {/* Experiment done modal. */} + + + {/* Map. */} { time={time} setActiveCar={setActiveCar} lineShapes={lineShapes} + showEditExperimentModal={showEditExperimentModal} + setShowEditExperimentModal={setShowEditExperimentModal} + experimentId={currentParameters.id} /> ) diff --git a/packages/visualisation/src/Map.jsx b/packages/visualisation/src/Map.jsx index 891555b1..414fec44 100644 --- a/packages/visualisation/src/Map.jsx +++ b/packages/visualisation/src/Map.jsx @@ -10,14 +10,10 @@ import DeckGL, { import { GeoJsonLayer } from '@deck.gl/layers' import inside from 'point-in-polygon' import { ParagraphLarge } from './components/Typography' - import KommunStatisticsBox from './components/KommunStatisticsBox' import TimeProgressBar from './components/TimeProgressBar' - import LayersMenu from './components/LayersMenu/index.jsx' -import mapboxgl from 'mapbox-gl' import HoverInfoBox from './components/HoverInfoBox' -import { IconButton, Menu, MenuItem } from '@mui/material' const transitionInterpolator = new LinearInterpolator(['bearing']) @@ -34,6 +30,8 @@ const Map = ({ activeCar, setActiveCar, time, + setShowEditExperimentModal, + experimentId, }) => { const [mapState, setMapState] = useState({ latitude: 65.0964472642777, @@ -379,7 +377,7 @@ const Map = ({ const bookingLayer = new ScatterplotLayer({ id: 'booking-layer', - data: bookings.filter(b => b.type !== 'busstop'), //.filter((b) => !b.assigned), // TODO: revert change + data: bookings.filter((b) => b.type !== 'busstop'), //.filter((b) => !b.assigned), // TODO: revert change opacity: 1, stroked: false, filled: true, @@ -619,12 +617,15 @@ const Map = ({ }} > {hoverInfo && mapState.zoom > 6 && } + + {/* Time progress bar. */} + + {/* Experiment clock. */}
i simuleringen
+ + {/* Municipality stats. */} {kommunInfo && } ) diff --git a/packages/visualisation/src/components/DropDown/index.jsx b/packages/visualisation/src/components/DropDown/index.jsx deleted file mode 100644 index 5059a348..00000000 --- a/packages/visualisation/src/components/DropDown/index.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useState } from 'react' -import styled from 'styled-components' -import { H2, H3 } from '../Typography' -import openArrow from '../../icons/svg/openArrow.svg' -import closeArrow from '../../icons/svg/closeArrow.svg' - -const Arrow = styled.button` - display: flex; - align-items: center; - justify-content: space-between; - border: none; - background-color: inherit; - cursor: pointer; - padding: 0; - gap: 1rem; - z-index: 5; -` - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - gap: 1rem; - margin-top: ${(props) => (props.small ? '1rem' : 0)}; - width: ${(props) => (props.small ? '90%' : '100%')}; -` - -const DropDown = ({ title, small, children }) => { - const [open, setOpen] = useState(false) - return ( - - setOpen((current) => !current)}> - {small ?

{title}

:

{title}

} - {open ? ( - Close - ) : ( - Open - )} -
- {open && children} -
- ) -} - -export default DropDown diff --git a/packages/visualisation/src/components/EditExperimentModal/index.jsx b/packages/visualisation/src/components/EditExperimentModal/index.jsx new file mode 100644 index 00000000..b64402db --- /dev/null +++ b/packages/visualisation/src/components/EditExperimentModal/index.jsx @@ -0,0 +1,75 @@ +import React from 'react' +import { Box, Button, Modal, Typography } from '@mui/material' +import { JsonEditor as Editor } from 'jsoneditor-react' + +const modalStyle = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 700, + height: 600, + bgcolor: 'white', + border: '2px solid #000', + boxShadow: 24, + p: 4, +} + +const editorStyle = { + border: '1px solid #dfdfdf', + height: 480, + overflow: 'scroll', +} + +const closeButtonStyle = { + marginLeft: '1rem', +} + +const bottomStyle = { + position: 'absolute', + bottom: '1rem', + width: '90%', + paddingRight: '1rem', +} + +const EditExperimentModal = ({ + fleets, + show, + setShow, + restartSimulation, + saveFleets, +}) => { + return ( + + + + Redigera Experiment + + + + + + + Dina ändringar träder i kraft när experimentet startar om. + + + + + + + ) +} + +export default EditExperimentModal diff --git a/packages/visualisation/src/components/ExperimentDoneModal/index.jsx b/packages/visualisation/src/components/ExperimentDoneModal/index.jsx new file mode 100644 index 00000000..644edebe --- /dev/null +++ b/packages/visualisation/src/components/ExperimentDoneModal/index.jsx @@ -0,0 +1,60 @@ +/* +let experimentUrl = + "https://kibana.predictivemovement.se/app/kibana#/dashboard/767e6ae0-3ac8-11ed-bade-51cc9f3c8210?_g=(refreshInterval:(pause:!t,value:0),time:(from:now%2Fd,to:now%2Fd))&_a=(description:'',filters:!(),fullScreenMode:!f,options:(hidePanelTitles:!f,useMargins:!t),panels:!((embeddableConfig:(mapCenter:!n,mapZoom:4),gridData:(h:15,i:'91cb1203-69c3-4398-8253-39ad96a9a9db',w:24,x:0,y:0),id:'65cf5470-3ac8-11ed-bade-51cc9f3c8210',panelIndex:'91cb1203-69c3-4398-8253-39ad96a9a9db',type:visualization,version:'7.5.1'),(embeddableConfig:(),gridData:(h:15,i:bfff9c2e-acd3-412a-a3ff-056b2bc25ddb,w:24,x:24,y:0),id:'57aab970-3ac8-11ed-bade-51cc9f3c8210',panelIndex:bfff9c2e-acd3-412a-a3ff-056b2bc25ddb,type:visualization,version:'7.5.1'),(embeddableConfig:(),gridData:(h:15,i:c47eb77d-b191-4a9b-9a04-127c581b4dcd,w:24,x:0,y:15),id:'9973b280-3ac8-11ed-bade-51cc9f3c8210',panelIndex:c47eb77d-b191-4a9b-9a04-127c581b4dcd,type:visualization,version:'7.5.1'),(embeddableConfig:(isLayerTOCOpen:!f,mapCenter:(lat:66.58123,lon:24.83098,zoom:4.37),openTOCDetails:!()),gridData:(h:15,i:c278a0ec-d11d-49d9-8606-2a9d3fb98342,w:24,x:24,y:15),id:c52ada20-3acd-11ed-bade-51cc9f3c8210,panelIndex:c278a0ec-d11d-49d9-8606-2a9d3fb98342,type:map,version:'7.5.1'),(embeddableConfig:(),gridData:(h:17,i:f2374bbc-382e-4266-be64-09c5def6c71e,w:48,x:0,y:30),id:'3f728310-3c21-11ed-bade-51cc9f3c8210',panelIndex:f2374bbc-382e-4266-be64-09c5def6c71e,type:visualization,version:'7.5.1')),query:(language:kuery,query:'EXPERIMENT_ID'),timeRestore:!f,title:Overview,viewMode:view)" + + experimentUrl = experimentUrl.replace(/EXPERIMENT_ID/, experimentId) + + window.open(experimentUrl, '_blank') +*/ + +import React from 'react' +import { Box, Button, Modal, Typography } from '@mui/material' + +const modalStyle = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 500, + bgcolor: 'background.paper', + color: 'white', + border: '2px solid #000', + boxShadow: 24, + p: 4, +} + +const ExperimentDoneModal = ({ experimentId, show, setShow }) => { + const openKibana = () => { + console.log('Open', experimentId) + window.open( + `https://kibana.predictivemovement.se/app/kibana#/dashboard/767e6ae0-3ac8-11ed-bade-51cc9f3c8210?_g=(refreshInterval:(pause:!t,value:0),time:(from:now%2Fd,to:now%2Fd))&_a=(description:'',filters:!(),fullScreenMode:!f,options:(hidePanelTitles:!f,useMargins:!t),panels:!((embeddableConfig:(mapCenter:!n,mapZoom:4),gridData:(h:15,i:'91cb1203-69c3-4398-8253-39ad96a9a9db',w:24,x:0,y:0),id:'65cf5470-3ac8-11ed-bade-51cc9f3c8210',panelIndex:'91cb1203-69c3-4398-8253-39ad96a9a9db',type:visualization,version:'7.5.1'),(embeddableConfig:(),gridData:(h:15,i:bfff9c2e-acd3-412a-a3ff-056b2bc25ddb,w:24,x:24,y:0),id:'57aab970-3ac8-11ed-bade-51cc9f3c8210',panelIndex:bfff9c2e-acd3-412a-a3ff-056b2bc25ddb,type:visualization,version:'7.5.1'),(embeddableConfig:(),gridData:(h:15,i:c47eb77d-b191-4a9b-9a04-127c581b4dcd,w:24,x:0,y:15),id:'9973b280-3ac8-11ed-bade-51cc9f3c8210',panelIndex:c47eb77d-b191-4a9b-9a04-127c581b4dcd,type:visualization,version:'7.5.1'),(embeddableConfig:(isLayerTOCOpen:!f,mapCenter:(lat:66.58123,lon:24.83098,zoom:4.37),openTOCDetails:!()),gridData:(h:15,i:c278a0ec-d11d-49d9-8606-2a9d3fb98342,w:24,x:24,y:15),id:c52ada20-3acd-11ed-bade-51cc9f3c8210,panelIndex:c278a0ec-d11d-49d9-8606-2a9d3fb98342,type:map,version:'7.5.1'),(embeddableConfig:(),gridData:(h:17,i:f2374bbc-382e-4266-be64-09c5def6c71e,w:48,x:0,y:30),id:'3f728310-3c21-11ed-bade-51cc9f3c8210',panelIndex:f2374bbc-382e-4266-be64-09c5def6c71e,type:visualization,version:'7.5.1')),query:(language:kuery,query:'${experimentId}'),timeRestore:!f,title:Overview,viewMode:view)`, + '_blank' + ) + } + + return ( + + + + Experiment klart + + + Nu har experimentet nått midnatt och startar om i bakgrunden. Du kan + se resultatet i Kibana. + + + + + + ) +} + +export default ExperimentDoneModal diff --git a/packages/visualisation/src/components/ExperimentSection/index.jsx b/packages/visualisation/src/components/ExperimentSection/index.jsx deleted file mode 100644 index 46d809a0..00000000 --- a/packages/visualisation/src/components/ExperimentSection/index.jsx +++ /dev/null @@ -1,257 +0,0 @@ -import styled from 'styled-components' -import Slider from '@mui/material/Slider' - -import { H1, H3, Paragraph, ParagraphBold } from '../Typography' -import checkIcon from '../../icons/svg/checkIcon.svg' - -const WhiteCircle = styled.div` - width: 22px; - height: 22px; - border-radius: 50%; - background-color: #ffffff; - border: 1px solid #ccc; -` - -const Circle = styled.div` - width: 24px; - height: 24px; - border-radius: 50%; - background-color: ${({ backgroundColor = 'transparent' }) => backgroundColor}; -` - -const Donut = styled.div` - width: 16px; - height: 16px; - border-radius: 50%; - border: 4px solid ${({ borderColor = 'transparent' }) => borderColor}; -` - -const MenuContainer = styled.div` - display: flex; - flex-direction: column; - gap: 2rem; - width: 300px; - background-color: white; - position: absolute; - bottom: 0; - left: 3.8rem; - padding-left: 2rem; - padding-right: 2rem; - padding-top: 2.5rem; - top: 0; - z-index: 4; - - overflow-y: scroll; -` - -const Button = styled.button` - width: 16px; - height: 16px; - background-color: #666666; - border: none; - border-radius: 2px; - cursor: pointer; - background-image: ${(props) => (props.checked ? `url(${checkIcon})` : null)}; - background-repeat: no-repeat; - background-position: center; -` - -const Flex = styled.div` - display: flex; - gap: 1rem; - align-items: center; -` - -const ParagraphMedium = styled(Paragraph)` - font-size: 14px; -` - -const FlexSpaceBetween = styled.div` - display: flex; - justify-content: space-between; -` - -const Container = styled.div` - display: flex; - flex-direction: column; - gap: 1.5rem; - margin-bottom: 2rem; -` - -const Grid = styled.div` - display: grid; - grid-template-columns: 1.5fr 1fr; - gap: 1rem 1.5rem; -` - -const StyledButton = styled.button` - width: 220px; - height: 40px; - background-color: #10c57b; - border: none; - z-index: 2; - cursor: pointer; - font-size: 14px; - font-family: Arial, Helvetica, sans-serif; - border-radius: 2px; -` - -const CheckItem = ({ text, setLayer, checked, color, borderOnly = false }) => { - return ( - -