summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2021-07-16 17:37:01 +0200
committerFabian Mastenbroek <mail.fabianm@gmail.com>2021-07-16 17:37:01 +0200
commitf2aeecccc096728d3df955b71e711c8d9c429427 (patch)
tree14494ef902f054a38f93af29976be81f8d5dba75 /opendc-web/opendc-web-ui
parente5caf6c6122684e441d1d73e2e0507fdd36c67e0 (diff)
refactor(ui): Isolate world coordinate space
This change updates the topology view in the OpenDC frontend to isolate the world coordinate space. This means that zooming and panning should not affect the coordinates in world space (but only in camera space). In turn, this allows us to remove the dependency on Redux for the camera controls.
Diffstat (limited to 'opendc-web/opendc-web-ui')
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js3
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/MapStage.js91
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/RackEnergyFillContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js12
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js26
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js67
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js20
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js26
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayer.js35
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js11
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayer.js50
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js6
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js12
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/map.js83
-rw-r--r--opendc-web/opendc-web-ui/src/redux/index.js3
-rw-r--r--opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js73
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/index.js2
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/map.js35
-rw-r--r--opendc-web/opendc-web-ui/src/util/effect-ref.js (renamed from opendc-web/opendc-web-ui/src/data/map.js)24
-rw-r--r--opendc-web/opendc-web-ui/yarn.lock14
22 files changed, 188 insertions, 432 deletions
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js b/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js
index 45799f70..4c3b2757 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js
@@ -12,9 +12,6 @@ export const WALL_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 16
export const OBJECT_BORDER_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 16
export const TILE_PLUS_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 10
-export const SIDEBAR_WIDTH = 350
-export const VIEWPORT_PADDING = 50
-
export const RACK_FILL_ICON_WIDTH = OBJECT_SIZE_IN_PIXELS / 3
export const RACK_FILL_ICON_OPACITY = 0.8
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapStage.js b/opendc-web/opendc-web-ui/src/components/app/map/MapStage.js
index 684ddf28..5d19b3ad 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/MapStage.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/MapStage.js
@@ -1,64 +1,81 @@
-import React, { useEffect, useRef, useState } from 'react'
+import React, { useRef, useState } from 'react'
import { HotKeys } from 'react-hotkeys'
import { Stage } from 'react-konva'
-import { MAP_MOVE_PIXELS_PER_EVENT } from './MapConstants'
-import { Provider, useDispatch, useStore } from 'react-redux'
+import { MAP_MAX_SCALE, MAP_MIN_SCALE, MAP_MOVE_PIXELS_PER_EVENT, MAP_SCALE_PER_EVENT } from './MapConstants'
+import { Provider, useStore } from 'react-redux'
import useResizeObserver from 'use-resize-observer'
import { mapContainer } from './MapStage.module.scss'
-import { useMapPosition } from '../../../data/map'
-import { setMapDimensions, setMapPositionWithBoundsCheck, zoomInOnPosition } from '../../../redux/actions/map'
import MapLayer from './layers/MapLayer'
import RoomHoverLayer from './layers/RoomHoverLayer'
import ObjectHoverLayer from './layers/ObjectHoverLayer'
+import ScaleIndicator from './controls/ScaleIndicator'
+import Toolbar from './controls/Toolbar'
function MapStage() {
const store = useStore()
- const dispatch = useDispatch()
+ const { ref, width = 100, height = 100 } = useResizeObserver()
+ const stageRef = useRef(null)
+ const [[x, y], setPos] = useState([0, 0])
+ const [scale, setScale] = useState(1)
+
+ const clampScale = (target) => Math.min(Math.max(target, MAP_MIN_SCALE), MAP_MAX_SCALE)
+ const moveWithDelta = (deltaX, deltaY) => setPos(([x, y]) => [x + deltaX, y + deltaY])
+
+ const onZoom = (e) => {
+ e.evt.preventDefault()
+
+ const stage = stageRef.current.getStage()
+ const oldScale = scale
+
+ const pointer = stage.getPointerPosition()
+ const mousePointTo = {
+ x: (pointer.x - x) / oldScale,
+ y: (pointer.y - y) / oldScale,
+ }
+
+ const newScale = clampScale(e.evt.deltaY > 0 ? oldScale * MAP_SCALE_PER_EVENT : oldScale / MAP_SCALE_PER_EVENT)
+
+ setScale(newScale)
+ setPos([pointer.x - mousePointTo.x * newScale, pointer.y - mousePointTo.y * newScale])
+ }
+ const onZoomButton = (zoomIn) =>
+ setScale((scale) => clampScale(zoomIn ? scale * MAP_SCALE_PER_EVENT : scale / MAP_SCALE_PER_EVENT))
+ const onDragEnd = (e) => setPos([e.target.x(), e.target.y()])
+ const onExport = () => {
+ const download = document.createElement('a')
+ download.href = stageRef.current.getStage().toDataURL()
+ download.download = 'opendc-canvas-export-' + Date.now() + '.png'
+ download.click()
+ }
- const stage = useRef(null)
- const [pos, setPos] = useState([0, 0])
- const [x, y] = pos
const handlers = {
MOVE_LEFT: () => moveWithDelta(MAP_MOVE_PIXELS_PER_EVENT, 0),
MOVE_RIGHT: () => moveWithDelta(-MAP_MOVE_PIXELS_PER_EVENT, 0),
MOVE_UP: () => moveWithDelta(0, MAP_MOVE_PIXELS_PER_EVENT),
MOVE_DOWN: () => moveWithDelta(0, -MAP_MOVE_PIXELS_PER_EVENT),
}
- const mapPosition = useMapPosition()
- const { ref, width = 100, height = 100 } = useResizeObserver()
-
- const moveWithDelta = (deltaX, deltaY) =>
- dispatch(setMapPositionWithBoundsCheck(mapPosition.x + deltaX, mapPosition.y + deltaY))
- const updateMousePosition = () => {
- if (!stage.current) {
- return
- }
-
- const mousePos = stage.current.getStage().getPointerPosition()
- setPos([mousePos.x, mousePos.y])
- }
- const updateScale = ({ evt }) => dispatch(zoomInOnPosition(evt.deltaY < 0, x, y))
-
- useEffect(() => {
- window['exportCanvasToImage'] = () => {
- const download = document.createElement('a')
- download.href = stage.current.getStage().toDataURL()
- download.download = 'opendc-canvas-export-' + Date.now() + '.png'
- download.click()
- }
- }, [stage])
-
- useEffect(() => dispatch(setMapDimensions(width, height)), [width, height]) // eslint-disable-line react-hooks/exhaustive-deps
return (
<HotKeys handlers={handlers} allowChanges={true} innerRef={ref} className={mapContainer}>
- <Stage ref={stage} width={width} height={height} onMouseMove={updateMousePosition} onWheel={updateScale} draggable>
+ <Stage
+ ref={stageRef}
+ onWheel={onZoom}
+ onDragEnd={onDragEnd}
+ draggable
+ width={width}
+ height={height}
+ scale={{ x: scale, y: scale }}
+ x={x}
+ y={y}
+ >
<Provider store={store}>
<MapLayer />
- <RoomHoverLayer mouseX={x} mouseY={y} />
- <ObjectHoverLayer mouseX={x} mouseY={y} />
+ <RoomHoverLayer />
+ <ObjectHoverLayer />
</Provider>
</Stage>
+ <ScaleIndicator scale={scale} />
+ <Toolbar onZoom={onZoomButton} onExport={onExport} />
</HotKeys>
)
}
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/RackEnergyFillContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/RackEnergyFillContainer.js
index 838aea5a..dbc26f14 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/RackEnergyFillContainer.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/RackEnergyFillContainer.js
@@ -3,10 +3,10 @@ import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
import RackFillBar from '../../../components/app/map/elements/RackFillBar'
-const RackSpaceFillContainer = (props) => {
- const state = useSelector((state) => {
+function RackSpaceFillContainer({ tileId, ...props }) {
+ const fillFraction = useSelector((state) => {
let energyConsumptionTotal = 0
- const rack = state.objects.rack[state.objects.tile[props.tileId].rack]
+ const rack = state.objects.rack[state.objects.tile[tileId].rack]
const machineIds = rack.machines
machineIds.forEach((machineId) => {
if (machineId !== null) {
@@ -22,12 +22,9 @@ const RackSpaceFillContainer = (props) => {
}
})
- return {
- type: 'energy',
- fillFraction: Math.min(1, energyConsumptionTotal / rack.powerCapacityW),
- }
+ return Math.min(1, energyConsumptionTotal / rack.powerCapacityW)
})
- return <RackFillBar {...props} {...state} />
+ return <RackFillBar {...props} type="energy" fillFraction={fillFraction} />
}
RackSpaceFillContainer.propTypes = {
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js
index 6791120e..7ca5c930 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js
@@ -25,15 +25,9 @@ import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
import RackFillBar from '../../../components/app/map/elements/RackFillBar'
-const RackSpaceFillContainer = (props) => {
- const state = useSelector((state) => {
- const machineIds = state.objects.rack[state.objects.tile[props.tileId].rack].machines
- return {
- type: 'space',
- fillFraction: machineIds.filter((id) => id !== null).length / machineIds.length,
- }
- })
- return <RackFillBar {...props} {...state} />
+function RackSpaceFillContainer({ tileId, ...props }) {
+ const rack = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack])
+ return <RackFillBar {...props} type="space" fillFraction={rack.machines.length / rack.capacity} />
}
RackSpaceFillContainer.propTypes = {
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js
index 11bba0e1..0369bb79 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js
@@ -4,10 +4,10 @@ import { Rect } from 'react-konva'
import { ROOM_HOVER_INVALID_COLOR, ROOM_HOVER_VALID_COLOR } from '../../../../util/colors'
import { TILE_SIZE_IN_PIXELS } from '../MapConstants'
-const HoverTile = ({ pixelX, pixelY, isValid, scale, onClick }) => (
+const HoverTile = ({ x, y, isValid, scale = 1, onClick }) => (
<Rect
- x={pixelX}
- y={pixelY}
+ x={x}
+ y={y}
scaleX={scale}
scaleY={scale}
width={TILE_SIZE_IN_PIXELS}
@@ -18,10 +18,10 @@ const HoverTile = ({ pixelX, pixelY, isValid, scale, onClick }) => (
)
HoverTile.propTypes = {
- pixelX: PropTypes.number.isRequired,
- pixelY: PropTypes.number.isRequired,
+ x: PropTypes.number.isRequired,
+ y: PropTypes.number.isRequired,
isValid: PropTypes.bool.isRequired,
- scale: PropTypes.number.isRequired,
+ scale: PropTypes.number,
onClick: PropTypes.func.isRequired,
}
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js
index 8c573a6f..aa284944 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js
@@ -16,7 +16,7 @@ import {
} from '../MapConstants'
import ImageComponent from './ImageComponent'
-const RackFillBar = ({ positionX, positionY, type, fillFraction }) => {
+function RackFillBar({ positionX, positionY, type, fillFraction }) {
const halfOfObjectBorderWidth = OBJECT_BORDER_WIDTH_IN_PIXELS / 2
const x =
positionX * TILE_SIZE_IN_PIXELS +
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js
index be3a00a8..186c2b3a 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js
@@ -4,19 +4,19 @@ import { Group, Line } from 'react-konva'
import { TILE_PLUS_COLOR } from '../../../../util/colors'
import { TILE_PLUS_MARGIN_IN_PIXELS, TILE_PLUS_WIDTH_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants'
-const TilePlusIcon = ({ pixelX, pixelY, mapScale }) => {
+function TilePlusIcon({ x, y, scale = 1 }) {
const linePoints = [
[
- pixelX + 0.5 * TILE_SIZE_IN_PIXELS * mapScale,
- pixelY + TILE_PLUS_MARGIN_IN_PIXELS * mapScale,
- pixelX + 0.5 * TILE_SIZE_IN_PIXELS * mapScale,
- pixelY + TILE_SIZE_IN_PIXELS * mapScale - TILE_PLUS_MARGIN_IN_PIXELS * mapScale,
+ x + 0.5 * TILE_SIZE_IN_PIXELS * scale,
+ y + TILE_PLUS_MARGIN_IN_PIXELS * scale,
+ x + 0.5 * TILE_SIZE_IN_PIXELS * scale,
+ y + TILE_SIZE_IN_PIXELS * scale - TILE_PLUS_MARGIN_IN_PIXELS * scale,
],
[
- pixelX + TILE_PLUS_MARGIN_IN_PIXELS * mapScale,
- pixelY + 0.5 * TILE_SIZE_IN_PIXELS * mapScale,
- pixelX + TILE_SIZE_IN_PIXELS * mapScale - TILE_PLUS_MARGIN_IN_PIXELS * mapScale,
- pixelY + 0.5 * TILE_SIZE_IN_PIXELS * mapScale,
+ x + TILE_PLUS_MARGIN_IN_PIXELS * scale,
+ y + 0.5 * TILE_SIZE_IN_PIXELS * scale,
+ x + TILE_SIZE_IN_PIXELS * scale - TILE_PLUS_MARGIN_IN_PIXELS * scale,
+ y + 0.5 * TILE_SIZE_IN_PIXELS * scale,
],
]
return (
@@ -27,7 +27,7 @@ const TilePlusIcon = ({ pixelX, pixelY, mapScale }) => {
points={points}
lineCap="round"
stroke={TILE_PLUS_COLOR}
- strokeWidth={TILE_PLUS_WIDTH_IN_PIXELS * mapScale}
+ strokeWidth={TILE_PLUS_WIDTH_IN_PIXELS * scale}
listening={false}
/>
))}
@@ -36,9 +36,9 @@ const TilePlusIcon = ({ pixelX, pixelY, mapScale }) => {
}
TilePlusIcon.propTypes = {
- pixelX: PropTypes.number,
- pixelY: PropTypes.number,
- mapScale: PropTypes.number,
+ x: PropTypes.number,
+ y: PropTypes.number,
+ scale: PropTypes.number,
}
export default TilePlusIcon
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js
index a88a8b34..2b1060c0 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js
@@ -1,61 +1,52 @@
import PropTypes from 'prop-types'
-import React, { useEffect, useState } from 'react'
-import { Layer } from 'react-konva'
+import React, { useMemo, useState } from 'react'
+import { Layer } from 'react-konva/lib/ReactKonva'
import HoverTile from '../elements/HoverTile'
import { TILE_SIZE_IN_PIXELS } from '../MapConstants'
+import { useEffectRef } from '../../../../util/effect-ref'
-function HoverLayerComponent({ mouseX, mouseY, mapPosition, mapScale, isEnabled, isValid, onClick, children }) {
- const [pos, setPos] = useState([-1, -1])
- const [x, y] = pos
- const [valid, setValid] = useState(false)
+function HoverLayerComponent({ isEnabled, isValid, onClick, children }) {
+ const [[mouseWorldX, mouseWorldY], setPos] = useState([0, 0])
- useEffect(() => {
- if (!isEnabled()) {
+ const layerRef = useEffectRef((layer) => {
+ if (!layer) {
return
}
- const positionX = Math.floor((mouseX - mapPosition.x) / (mapScale * TILE_SIZE_IN_PIXELS))
- const positionY = Math.floor((mouseY - mapPosition.y) / (mapScale * TILE_SIZE_IN_PIXELS))
+ const stage = layer.getStage()
- if (positionX !== x || positionY !== y) {
- setPos([positionX, positionY])
- setValid(isValid(positionX, positionY))
- }
- }, [isEnabled, isValid, x, y, mouseX, mouseY, mapPosition, mapScale])
+ // Transform used to convert mouse coordinates to world coordinates
+ const transform = stage.getAbsoluteTransform().copy()
+ transform.invert()
+
+ stage.on('mousemove.hover', () => {
+ const { x, y } = transform.point(stage.getPointerPosition())
+ setPos([x, y])
+ })
+ return () => stage.off('mousemove.hover')
+ })
+
+ const gridX = Math.floor(mouseWorldX / TILE_SIZE_IN_PIXELS)
+ const gridY = Math.floor(mouseWorldY / TILE_SIZE_IN_PIXELS)
+ const valid = useMemo(() => isEnabled && isValid(gridX, gridY), [isEnabled, isValid, gridX, gridY])
- if (!isEnabled()) {
+ if (!isEnabled) {
return <Layer />
}
- const pixelX = mapScale * x * TILE_SIZE_IN_PIXELS + mapPosition.x
- const pixelY = mapScale * y * TILE_SIZE_IN_PIXELS + mapPosition.y
+ const x = gridX * TILE_SIZE_IN_PIXELS
+ const y = gridY * TILE_SIZE_IN_PIXELS
return (
- <Layer opacity={0.6}>
- <HoverTile
- pixelX={pixelX}
- pixelY={pixelY}
- scale={mapScale}
- isValid={valid}
- onClick={() => (valid ? onClick(x, y) : undefined)}
- />
- {children
- ? React.cloneElement(children, {
- pixelX,
- pixelY,
- scale: mapScale,
- })
- : undefined}
+ <Layer opacity={0.6} ref={layerRef}>
+ <HoverTile x={x} y={y} isValid={valid} onClick={() => (valid ? onClick(gridX, gridY) : undefined)} />
+ {children ? React.cloneElement(children, { x, y, scale: 1 }) : undefined}
</Layer>
)
}
HoverLayerComponent.propTypes = {
- mouseX: PropTypes.number.isRequired,
- mouseY: PropTypes.number.isRequired,
- mapPosition: PropTypes.object.isRequired,
- mapScale: PropTypes.number.isRequired,
- isEnabled: PropTypes.func.isRequired,
+ isEnabled: PropTypes.bool.isRequired,
isValid: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
children: PropTypes.node,
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js
index badb9f68..c902532b 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js
@@ -21,13 +21,21 @@
*/
import React from 'react'
-import MapLayerComponent from '../../../../components/app/map/layers/MapLayerComponent'
-import { useMapPosition, useMapScale } from '../../../../data/map'
+import { Group, Layer } from 'react-konva'
+import Backdrop from '../elements/Backdrop'
+import TopologyContainer from '../TopologyContainer'
+import GridGroup from '../groups/GridGroup'
-const MapLayer = (props) => {
- const position = useMapPosition()
- const scale = useMapScale()
- return <MapLayerComponent {...props} mapPosition={position} mapScale={scale} />
+function MapLayer() {
+ return (
+ <Layer>
+ <Group>
+ <Backdrop />
+ <TopologyContainer />
+ <GridGroup />
+ </Group>
+ </Layer>
+ )
}
export default MapLayer
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js
deleted file mode 100644
index efe5b4e5..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { Group, Layer } from 'react-konva'
-import Backdrop from '../elements/Backdrop'
-import GridGroup from '../groups/GridGroup'
-import TopologyContainer from '../TopologyContainer'
-
-const MapLayerComponent = ({ mapPosition, mapScale }) => (
- <Layer>
- <Group x={mapPosition.x} y={mapPosition.y} scaleX={mapScale} scaleY={mapScale}>
- <Backdrop />
- <TopologyContainer />
- <GridGroup />
- </Group>
- </Layer>
-)
-
-MapLayerComponent.propTypes = {
- mapPosition: PropTypes.shape({
- x: PropTypes.number,
- y: PropTypes.number,
- }),
- mapScale: PropTypes.number,
-}
-
-export default MapLayerComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayer.js
index 9a087bd5..47d9c992 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayer.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayer.js
@@ -23,32 +23,31 @@
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { addRackToTile } from '../../../../redux/actions/topology/room'
-import ObjectHoverLayerComponent from '../../../../components/app/map/layers/ObjectHoverLayerComponent'
import { findTileWithPosition } from '../../../../util/tile-calculations'
+import HoverLayerComponent from './HoverLayerComponent'
+import TilePlusIcon from '../elements/TilePlusIcon'
-const ObjectHoverLayer = (props) => {
- const state = useSelector((state) => {
- return {
- mapPosition: state.map.position,
- mapScale: state.map.scale,
- isEnabled: () => state.construction.inRackConstructionMode,
- isValid: (x, y) => {
- if (state.interactionLevel.mode !== 'ROOM') {
- return false
- }
+function ObjectHoverLayer() {
+ const isEnabled = useSelector((state) => state.construction.inRackConstructionMode)
+ const isValid = useSelector((state) => (x, y) => {
+ if (state.interactionLevel.mode !== 'ROOM') {
+ return false
+ }
- const currentRoom = state.objects.room[state.interactionLevel.roomId]
- const tiles = currentRoom.tiles.map((tileId) => state.objects.tile[tileId])
- const tile = findTileWithPosition(tiles, x, y)
+ const currentRoom = state.objects.room[state.interactionLevel.roomId]
+ const tiles = currentRoom.tiles.map((tileId) => state.objects.tile[tileId])
+ const tile = findTileWithPosition(tiles, x, y)
- return !(tile === null || tile.rack)
- },
- }
+ return !(tile === null || tile.rack)
})
const dispatch = useDispatch()
const onClick = (x, y) => dispatch(addRackToTile(x, y))
- return <ObjectHoverLayerComponent {...props} {...state} onClick={onClick} />
+ return (
+ <HoverLayerComponent onClick={onClick} isEnabled={isEnabled} isValid={isValid}>
+ <TilePlusIcon />
+ </HoverLayerComponent>
+ )
}
export default ObjectHoverLayer
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js
deleted file mode 100644
index 661fc255..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react'
-import TilePlusIcon from '../elements/TilePlusIcon'
-import HoverLayerComponent from './HoverLayerComponent'
-
-const ObjectHoverLayerComponent = (props) => (
- <HoverLayerComponent {...props}>
- <TilePlusIcon {...props} />
- </HoverLayerComponent>
-)
-
-export default ObjectHoverLayerComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayer.js
index 87240813..59f83b2b 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayer.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayer.js
@@ -23,45 +23,39 @@
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { toggleTileAtLocation } from '../../../../redux/actions/topology/building'
-import RoomHoverLayerComponent from '../../../../components/app/map/layers/RoomHoverLayerComponent'
import {
deriveValidNextTilePositions,
findPositionInPositions,
findPositionInRooms,
} from '../../../../util/tile-calculations'
+import HoverLayerComponent from './HoverLayerComponent'
-const RoomHoverLayer = (props) => {
+function RoomHoverLayer() {
const dispatch = useDispatch()
const onClick = (x, y) => dispatch(toggleTileAtLocation(x, y))
+ const isEnabled = useSelector((state) => state.construction.currentRoomInConstruction !== '-1')
+ const isValid = useSelector((state) => (x, y) => {
+ const newRoom = { ...state.objects.room[state.construction.currentRoomInConstruction] }
+ const oldRooms = Object.keys(state.objects.room)
+ .map((id) => ({ ...state.objects.room[id] }))
+ .filter(
+ (room) =>
+ state.objects.topology[state.currentTopologyId].rooms.indexOf(room._id) !== -1 &&
+ room._id !== state.construction.currentRoomInConstruction
+ )
- const state = useSelector((state) => {
- return {
- mapPosition: state.map.position,
- mapScale: state.map.scale,
- isEnabled: () => state.construction.currentRoomInConstruction !== '-1',
- isValid: (x, y) => {
- const newRoom = Object.assign({}, state.objects.room[state.construction.currentRoomInConstruction])
- const oldRooms = Object.keys(state.objects.room)
- .map((id) => Object.assign({}, state.objects.room[id]))
- .filter(
- (room) =>
- state.objects.topology[state.currentTopologyId].rooms.indexOf(room._id) !== -1 &&
- room._id !== state.construction.currentRoomInConstruction
- )
-
- ;[...oldRooms, newRoom].forEach((room) => {
- room.tiles = room.tiles.map((tileId) => state.objects.tile[tileId])
- })
- if (newRoom.tiles.length === 0) {
- return findPositionInRooms(oldRooms, x, y) === -1
- }
-
- const validNextPositions = deriveValidNextTilePositions(oldRooms, newRoom.tiles)
- return findPositionInPositions(validNextPositions, x, y) !== -1
- },
+ ;[...oldRooms, newRoom].forEach((room) => {
+ room.tiles = room.tiles.map((tileId) => state.objects.tile[tileId])
+ })
+ if (newRoom.tiles.length === 0) {
+ return findPositionInRooms(oldRooms, x, y) === -1
}
+
+ const validNextPositions = deriveValidNextTilePositions(oldRooms, newRoom.tiles)
+ return findPositionInPositions(validNextPositions, x, y) !== -1
})
- return <RoomHoverLayerComponent onClick={onClick} {...props} {...state} />
+
+ return <HoverLayerComponent onClick={onClick} isEnabled={isEnabled} isValid={isValid} />
}
export default RoomHoverLayer
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js
deleted file mode 100644
index 887e2891..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import React from 'react'
-import HoverLayerComponent from './HoverLayerComponent'
-
-const RoomHoverLayerComponent = (props) => <HoverLayerComponent {...props} />
-
-export default RoomHoverLayerComponent
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
index 786bed07..d79e8e7a 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
@@ -24,7 +24,7 @@ import { useRouter } from 'next/router'
import { useProject } from '../../../../data/project'
import { useDispatch, useSelector } from 'react-redux'
import React, { useEffect, useState } from 'react'
-import {configure, HotKeys} from 'react-hotkeys'
+import { configure, HotKeys } from 'react-hotkeys'
import { KeymapConfiguration } from '../../../../hotkeys'
import Head from 'next/head'
import MapStage from '../../../../components/app/map/MapStage'
@@ -40,9 +40,7 @@ import {
Spinner,
Title,
} from '@patternfly/react-core'
-import { zoomInOnCenter } from '../../../../redux/actions/map'
import Toolbar from '../../../../components/app/map/controls/Toolbar'
-import { useMapScale } from '../../../../data/map'
import ScaleIndicator from '../../../../components/app/map/controls/ScaleIndicator'
import TopologySidebar from '../../../../components/app/sidebars/topology/TopologySidebar'
import Collapse from '../../../../components/app/map/controls/Collapse'
@@ -64,7 +62,6 @@ function Topology() {
}, [projectId, topologyId, dispatch])
const topologyIsLoading = useSelector((state) => state.currentTopologyId === '-1')
- const scale = useMapScale()
const interactionLevel = useSelector((state) => state.interactionLevel)
const [isExpanded, setExpanded] = useState(true)
@@ -72,7 +69,7 @@ function Topology() {
// Make sure that holding down a key will generate repeated events
configure({
- ignoreRepeatedEventsWhenKeyHeldDown: false
+ ignoreRepeatedEventsWhenKeyHeldDown: false,
})
return (
@@ -95,11 +92,6 @@ function Topology() {
<DrawerContent panelContent={panelContent}>
<DrawerContentBody>
<MapStage />
- <ScaleIndicator scale={scale} />
- <Toolbar
- onZoom={(zoomIn) => dispatch(zoomInOnCenter(zoomIn))}
- onExport={() => window['exportCanvasToImage']()}
- />
<Collapse onClick={() => setExpanded(true)} />
</DrawerContentBody>
</DrawerContent>
diff --git a/opendc-web/opendc-web-ui/src/redux/actions/map.js b/opendc-web/opendc-web-ui/src/redux/actions/map.js
deleted file mode 100644
index aa14cacd..00000000
--- a/opendc-web/opendc-web-ui/src/redux/actions/map.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import {
- MAP_MAX_SCALE,
- MAP_MIN_SCALE,
- MAP_SCALE_PER_EVENT,
- MAP_SIZE_IN_PIXELS,
-} from '../../components/app/map/MapConstants'
-
-export const SET_MAP_POSITION = 'SET_MAP_POSITION'
-export const SET_MAP_DIMENSIONS = 'SET_MAP_DIMENSIONS'
-export const SET_MAP_SCALE = 'SET_MAP_SCALE'
-
-export function setMapPosition(x, y) {
- return {
- type: SET_MAP_POSITION,
- x,
- y,
- }
-}
-
-export function setMapDimensions(width, height) {
- return {
- type: SET_MAP_DIMENSIONS,
- width,
- height,
- }
-}
-
-export function setMapScale(scale) {
- return {
- type: SET_MAP_SCALE,
- scale,
- }
-}
-
-export function zoomInOnCenter(zoomIn) {
- return (dispatch, getState) => {
- const state = getState()
-
- dispatch(zoomInOnPosition(zoomIn, state.map.dimensions.width / 2, state.map.dimensions.height / 2))
- }
-}
-
-export function zoomInOnPosition(zoomIn, x, y) {
- return (dispatch, getState) => {
- const state = getState()
-
- const centerPoint = {
- x: x / state.map.scale - state.map.position.x / state.map.scale,
- y: y / state.map.scale - state.map.position.y / state.map.scale,
- }
- const newScale = zoomIn ? state.map.scale * MAP_SCALE_PER_EVENT : state.map.scale / MAP_SCALE_PER_EVENT
- const boundedScale = Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE)
-
- const newX = -(centerPoint.x - x / boundedScale) * boundedScale
- const newY = -(centerPoint.y - y / boundedScale) * boundedScale
-
- dispatch(setMapPositionWithBoundsCheck(newX, newY))
- dispatch(setMapScale(boundedScale))
- }
-}
-
-export function setMapPositionWithBoundsCheck(x, y) {
- return (dispatch, getState) => {
- const state = getState()
-
- const scaledMapSize = MAP_SIZE_IN_PIXELS * state.map.scale
-
- const updatedX =
- x > 0
- ? 0
- : x < -scaledMapSize + state.map.dimensions.width
- ? -scaledMapSize + state.map.dimensions.width
- : x
- const updatedY =
- y > 0
- ? 0
- : y < -scaledMapSize + state.map.dimensions.height
- ? -scaledMapSize + state.map.dimensions.height
- : y
-
- dispatch(setMapPosition(updatedX, updatedY))
- }
-}
diff --git a/opendc-web/opendc-web-ui/src/redux/index.js b/opendc-web/opendc-web-ui/src/redux/index.js
index 3c7ad55f..fa0c9d23 100644
--- a/opendc-web/opendc-web-ui/src/redux/index.js
+++ b/opendc-web/opendc-web-ui/src/redux/index.js
@@ -5,7 +5,6 @@ import createSagaMiddleware from 'redux-saga'
import thunk from 'redux-thunk'
import rootReducer from './reducers'
import rootSaga from './sagas'
-import { viewportAdjustmentMiddleware } from './middleware/viewport-adjustment'
import { createReduxEnhancer } from '@sentry/react'
let store
@@ -13,7 +12,7 @@ let store
function initStore(initialState, ctx) {
const sagaMiddleware = createSagaMiddleware({ context: ctx })
- const middlewares = [thunk, sagaMiddleware, viewportAdjustmentMiddleware]
+ const middlewares = [thunk, sagaMiddleware]
if (process.env.NODE_ENV !== 'production') {
middlewares.push(createLogger())
diff --git a/opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js b/opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js
deleted file mode 100644
index c2fc5004..00000000
--- a/opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import { SET_MAP_DIMENSIONS, setMapPosition, setMapScale } from '../actions/map'
-import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building'
-import {
- MAP_MAX_SCALE,
- MAP_MIN_SCALE,
- SIDEBAR_WIDTH,
- TILE_SIZE_IN_PIXELS,
- VIEWPORT_PADDING,
-} from '../../components/app/map/MapConstants'
-import { calculateRoomListBounds } from '../../util/tile-calculations'
-
-export const viewportAdjustmentMiddleware = (store) => (next) => (action) => {
- const state = store.getState()
-
- let topologyId = '-1'
- let mapDimensions = {}
- if (action.type === SET_CURRENT_TOPOLOGY && action.topologyId !== '-1') {
- topologyId = action.topologyId
- mapDimensions = state.map.dimensions
- } else if (action.type === SET_MAP_DIMENSIONS && state.currentTopologyId !== '-1') {
- topologyId = state.currentTopologyId
- mapDimensions = { width: action.width, height: action.height }
- }
-
- if (topologyId && topologyId !== '-1') {
- const roomIds = state.objects.topology[topologyId].rooms
- const rooms = roomIds.map((id) => Object.assign({}, state.objects.room[id]))
- rooms.forEach((room) => (room.tiles = room.tiles.map((tileId) => state.objects.tile[tileId])))
-
- let hasNoTiles = true
- for (let i in rooms) {
- if (rooms[i].tiles.length > 0) {
- hasNoTiles = false
- break
- }
- }
-
- if (!hasNoTiles) {
- const viewportParams = calculateParametersToZoomInOnRooms(rooms, mapDimensions.width, mapDimensions.height)
- store.dispatch(setMapPosition(viewportParams.newX, viewportParams.newY))
- store.dispatch(setMapScale(viewportParams.newScale))
- }
- }
-
- next(action)
-}
-
-function calculateParametersToZoomInOnRooms(rooms, mapWidth, mapHeight) {
- const bounds = calculateRoomListBounds(rooms)
- const newScale = calculateNewScale(bounds, mapWidth, mapHeight)
-
- // Coordinates of the center of the room, relative to the global origin of the map
- const roomCenterCoordinates = {
- x: bounds.center.x * TILE_SIZE_IN_PIXELS * newScale,
- y: bounds.center.y * TILE_SIZE_IN_PIXELS * newScale,
- }
-
- const newX = -roomCenterCoordinates.x + mapWidth / 2
- const newY = -roomCenterCoordinates.y + mapHeight / 2
-
- return { newScale, newX, newY }
-}
-
-function calculateNewScale(bounds, mapWidth, mapHeight) {
- const width = bounds.max.x - bounds.min.x
- const height = bounds.max.y - bounds.min.y
-
- const scaleX = (mapWidth - 2 * SIDEBAR_WIDTH) / (width * TILE_SIZE_IN_PIXELS + 2 * VIEWPORT_PADDING)
- const scaleY = mapHeight / (height * TILE_SIZE_IN_PIXELS + 2 * VIEWPORT_PADDING)
- const newScale = Math.min(scaleX, scaleY)
-
- return Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE)
-}
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/index.js b/opendc-web/opendc-web-ui/src/redux/reducers/index.js
index 1b17a206..2f1359d6 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/index.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/index.js
@@ -2,13 +2,11 @@ import { combineReducers } from 'redux'
import { construction } from './construction-mode'
import { currentTopologyId } from './current-ids'
import { interactionLevel } from './interaction-level'
-import { map } from './map'
import { objects } from './objects'
const rootReducer = combineReducers({
objects,
construction,
- map,
currentTopologyId,
interactionLevel,
})
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/map.js b/opendc-web/opendc-web-ui/src/redux/reducers/map.js
deleted file mode 100644
index de712c15..00000000
--- a/opendc-web/opendc-web-ui/src/redux/reducers/map.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { combineReducers } from 'redux'
-import { SET_MAP_DIMENSIONS, SET_MAP_POSITION, SET_MAP_SCALE } from '../actions/map'
-
-export function position(state = { x: 0, y: 0 }, action) {
- switch (action.type) {
- case SET_MAP_POSITION:
- return { x: action.x, y: action.y }
- default:
- return state
- }
-}
-
-export function dimensions(state = { width: 600, height: 400 }, action) {
- switch (action.type) {
- case SET_MAP_DIMENSIONS:
- return { width: action.width, height: action.height }
- default:
- return state
- }
-}
-
-export function scale(state = 1, action) {
- switch (action.type) {
- case SET_MAP_SCALE:
- return action.scale
- default:
- return state
- }
-}
-
-export const map = combineReducers({
- position,
- dimensions,
- scale,
-})
diff --git a/opendc-web/opendc-web-ui/src/data/map.js b/opendc-web/opendc-web-ui/src/util/effect-ref.js
index 348a6664..cda0324b 100644
--- a/opendc-web/opendc-web-ui/src/data/map.js
+++ b/opendc-web/opendc-web-ui/src/util/effect-ref.js
@@ -20,18 +20,22 @@
* SOFTWARE.
*/
-import { useSelector } from 'react-redux'
+import { useCallback, useRef } from 'react'
-/**
- * Return the map scale.
- */
-export function useMapScale() {
- return useSelector((state) => state.map.scale)
-}
+const noop = () => {}
/**
- * Return the map position.
+ * A hook that will invoke the specified callback when the reference returned by this function is initialized.
+ * The callback can return an optional clean up function.
*/
-export function useMapPosition() {
- return useSelector((state) => state.map.position)
+export function useEffectRef(callback) {
+ const disposeRef = useRef(noop)
+ return useCallback((element) => {
+ disposeRef.current()
+ disposeRef.current = noop
+
+ if (element) {
+ disposeRef.current = callback(element) || noop
+ }
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
}
diff --git a/opendc-web/opendc-web-ui/yarn.lock b/opendc-web/opendc-web-ui/yarn.lock
index cd446e99..b0640a01 100644
--- a/opendc-web/opendc-web-ui/yarn.lock
+++ b/opendc-web/opendc-web-ui/yarn.lock
@@ -3337,12 +3337,12 @@ react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1:
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-konva@~17.0.2-0:
- version "17.0.2-0"
- resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-17.0.2-0.tgz#35b1cf1b0ff3ed63b7de16aa6c0744e496391374"
- integrity sha512-zT//biPf2UqqE4OIaivY2NfnnYgsEscSFacO9sbiUVrhtaP9mV8YgKx48CA4XjlgvfjjZqAxY6JTTmjJlsVYPw==
+ version "17.0.2-5"
+ resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-17.0.2-5.tgz#e70b0acf323402de0a540f27b300fbe7ed151849"
+ integrity sha512-IyzdfqRDK8r1ulp/jbLPX18AuO+n5yNtL0+4T0QEUsgArRqIl/VRCG1imA5mYJBk0cBNC5+fWDHN+HWEW62ZEQ==
dependencies:
- react-reconciler "~0.26.1"
- scheduler "^0.20.1"
+ react-reconciler "~0.26.2"
+ scheduler "^0.20.2"
react-lifecycles-compat@^3.0.4:
version "3.0.4"
@@ -3358,7 +3358,7 @@ react-query@^3.18.1:
broadcast-channel "^3.4.1"
match-sorter "^6.0.2"
-react-reconciler@~0.26.1:
+react-reconciler@~0.26.2:
version "0.26.2"
resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.26.2.tgz#bbad0e2d1309423f76cf3c3309ac6c96e05e9d91"
integrity sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q==
@@ -3653,7 +3653,7 @@ sass@^1.32.12:
dependencies:
chokidar ">=3.0.0 <4.0.0"
-scheduler@^0.20.1, scheduler@^0.20.2:
+scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==