summaryrefslogtreecommitdiff
path: root/frontend/src/components/app/map
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/app/map')
-rw-r--r--frontend/src/components/app/map/LoadingScreen.js11
-rw-r--r--frontend/src/components/app/map/MapConstants.js29
-rw-r--r--frontend/src/components/app/map/MapStageComponent.js126
-rw-r--r--frontend/src/components/app/map/controls/ExportCanvasComponent.js13
-rw-r--r--frontend/src/components/app/map/controls/ScaleIndicatorComponent.js14
-rw-r--r--frontend/src/components/app/map/controls/ScaleIndicatorComponent.sass9
-rw-r--r--frontend/src/components/app/map/controls/ToolPanelComponent.js13
-rw-r--r--frontend/src/components/app/map/controls/ToolPanelComponent.sass5
-rw-r--r--frontend/src/components/app/map/controls/ZoomControlComponent.js24
-rw-r--r--frontend/src/components/app/map/elements/Backdrop.js16
-rw-r--r--frontend/src/components/app/map/elements/GrayLayer.js17
-rw-r--r--frontend/src/components/app/map/elements/HoverTile.js30
-rw-r--r--frontend/src/components/app/map/elements/ImageComponent.js48
-rw-r--r--frontend/src/components/app/map/elements/RackFillBar.js89
-rw-r--r--frontend/src/components/app/map/elements/RoomTile.js20
-rw-r--r--frontend/src/components/app/map/elements/TileObject.js29
-rw-r--r--frontend/src/components/app/map/elements/TilePlusIcon.js52
-rw-r--r--frontend/src/components/app/map/elements/WallSegment.js39
-rw-r--r--frontend/src/components/app/map/groups/DatacenterGroup.js40
-rw-r--r--frontend/src/components/app/map/groups/GridGroup.js41
-rw-r--r--frontend/src/components/app/map/groups/RackGroup.js43
-rw-r--r--frontend/src/components/app/map/groups/RoomGroup.js56
-rw-r--r--frontend/src/components/app/map/groups/TileGroup.js43
-rw-r--r--frontend/src/components/app/map/groups/WallGroup.js22
-rw-r--r--frontend/src/components/app/map/layers/HoverLayerComponent.js85
-rw-r--r--frontend/src/components/app/map/layers/MapLayerComponent.js22
-rw-r--r--frontend/src/components/app/map/layers/ObjectHoverLayerComponent.js11
-rw-r--r--frontend/src/components/app/map/layers/RoomHoverLayerComponent.js6
28 files changed, 953 insertions, 0 deletions
diff --git a/frontend/src/components/app/map/LoadingScreen.js b/frontend/src/components/app/map/LoadingScreen.js
new file mode 100644
index 00000000..9f379e0b
--- /dev/null
+++ b/frontend/src/components/app/map/LoadingScreen.js
@@ -0,0 +1,11 @@
+import React from "react";
+import FontAwesome from "react-fontawesome";
+
+const LoadingScreen = () => (
+ <div className="display-4">
+ <FontAwesome name="refresh" className="mr-4" spin />
+ Loading your datacenter...
+ </div>
+);
+
+export default LoadingScreen;
diff --git a/frontend/src/components/app/map/MapConstants.js b/frontend/src/components/app/map/MapConstants.js
new file mode 100644
index 00000000..32438b5e
--- /dev/null
+++ b/frontend/src/components/app/map/MapConstants.js
@@ -0,0 +1,29 @@
+export const MAP_SIZE = 50;
+export const TILE_SIZE_IN_PIXELS = 100;
+export const TILE_SIZE_IN_METERS = 0.5;
+export const MAP_SIZE_IN_PIXELS = MAP_SIZE * TILE_SIZE_IN_PIXELS;
+
+export const OBJECT_MARGIN_IN_PIXELS = TILE_SIZE_IN_PIXELS / 5;
+export const TILE_PLUS_MARGIN_IN_PIXELS = TILE_SIZE_IN_PIXELS / 3;
+export const OBJECT_SIZE_IN_PIXELS =
+ TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2;
+
+export const GRID_LINE_WIDTH_IN_PIXELS = 2;
+export const WALL_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 8;
+export const OBJECT_BORDER_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 12;
+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;
+
+export const MAP_MOVE_PIXELS_PER_EVENT = 20;
+export const MAP_SCALE_PER_EVENT = 1.1;
+export const MAP_MIN_SCALE = 0.5;
+export const MAP_MAX_SCALE = 1.5;
+
+export const MAX_NUM_UNITS_PER_MACHINE = 4;
+export const DEFAULT_RACK_SLOT_CAPACITY = 42;
+export const DEFAULT_RACK_POWER_CAPACITY = 10000;
diff --git a/frontend/src/components/app/map/MapStageComponent.js b/frontend/src/components/app/map/MapStageComponent.js
new file mode 100644
index 00000000..67b3349c
--- /dev/null
+++ b/frontend/src/components/app/map/MapStageComponent.js
@@ -0,0 +1,126 @@
+import React from "react";
+import { Stage } from "react-konva";
+import { Shortcuts } from "react-shortcuts";
+import MapLayer from "../../../containers/app/map/layers/MapLayer";
+import ObjectHoverLayer from "../../../containers/app/map/layers/ObjectHoverLayer";
+import RoomHoverLayer from "../../../containers/app/map/layers/RoomHoverLayer";
+import jQuery from "../../../util/jquery";
+import { NAVBAR_HEIGHT } from "../../navigation/Navbar";
+import { MAP_MOVE_PIXELS_PER_EVENT } from "./MapConstants";
+import { Provider } from "react-redux";
+import { store } from "../../../store/configure-store";
+
+class MapStageComponent extends React.Component {
+ state = {
+ mouseX: 0,
+ mouseY: 0
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.updateDimensions = this.updateDimensions.bind(this);
+ this.updateScale = this.updateScale.bind(this);
+ }
+
+ componentWillMount() {
+ this.updateDimensions();
+ }
+
+ componentDidMount() {
+ window.addEventListener("resize", this.updateDimensions);
+ window.addEventListener("wheel", this.updateScale);
+
+ window["exportCanvasToImage"] = () => {
+ const download = document.createElement("a");
+ download.href = this.stage.getStage().toDataURL();
+ download.download = "opendc-canvas-export-" + Date.now() + ".png";
+ download.click();
+ };
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this.updateDimensions);
+ window.removeEventListener("wheel", this.updateScale);
+ }
+
+ updateDimensions() {
+ this.props.setMapDimensions(
+ jQuery(window).width(),
+ jQuery(window).height() - NAVBAR_HEIGHT
+ );
+ }
+
+ updateScale(e) {
+ e.preventDefault();
+ this.props.zoomInOnPosition(
+ e.deltaY < 0,
+ this.state.mouseX,
+ this.state.mouseY
+ );
+ }
+
+ updateMousePosition() {
+ const mousePos = this.stage.getStage().getPointerPosition();
+ this.setState({ mouseX: mousePos.x, mouseY: mousePos.y });
+ }
+
+ handleShortcuts(action) {
+ switch (action) {
+ case "MOVE_LEFT":
+ this.moveWithDelta(MAP_MOVE_PIXELS_PER_EVENT, 0);
+ break;
+ case "MOVE_RIGHT":
+ this.moveWithDelta(-MAP_MOVE_PIXELS_PER_EVENT, 0);
+ break;
+ case "MOVE_UP":
+ this.moveWithDelta(0, MAP_MOVE_PIXELS_PER_EVENT);
+ break;
+ case "MOVE_DOWN":
+ this.moveWithDelta(0, -MAP_MOVE_PIXELS_PER_EVENT);
+ break;
+ default:
+ break;
+ }
+ }
+
+ moveWithDelta(deltaX, deltaY) {
+ this.props.setMapPositionWithBoundsCheck(
+ this.props.mapPosition.x + deltaX,
+ this.props.mapPosition.y + deltaY
+ );
+ }
+
+ render() {
+ return (
+ <Shortcuts
+ name="MAP"
+ handler={this.handleShortcuts.bind(this)}
+ targetNodeSelector="body"
+ >
+ <Stage
+ ref={stage => {
+ this.stage = stage;
+ }}
+ width={this.props.mapDimensions.width}
+ height={this.props.mapDimensions.height}
+ onMouseMove={this.updateMousePosition.bind(this)}
+ >
+ <Provider store={store}>
+ <MapLayer />
+ <RoomHoverLayer
+ mouseX={this.state.mouseX}
+ mouseY={this.state.mouseY}
+ />
+ <ObjectHoverLayer
+ mouseX={this.state.mouseX}
+ mouseY={this.state.mouseY}
+ />
+ </Provider>
+ </Stage>
+ </Shortcuts>
+ );
+ }
+}
+
+export default MapStageComponent;
diff --git a/frontend/src/components/app/map/controls/ExportCanvasComponent.js b/frontend/src/components/app/map/controls/ExportCanvasComponent.js
new file mode 100644
index 00000000..ee934f21
--- /dev/null
+++ b/frontend/src/components/app/map/controls/ExportCanvasComponent.js
@@ -0,0 +1,13 @@
+import React from "react";
+
+const ExportCanvasComponent = () => (
+ <button
+ className="btn btn-success btn-circle btn-sm"
+ title="Export Canvas to PNG Image"
+ onClick={() => window["exportCanvasToImage"]()}
+ >
+ <span className="fa fa-camera" />
+ </button>
+);
+
+export default ExportCanvasComponent;
diff --git a/frontend/src/components/app/map/controls/ScaleIndicatorComponent.js b/frontend/src/components/app/map/controls/ScaleIndicatorComponent.js
new file mode 100644
index 00000000..b7b5cc36
--- /dev/null
+++ b/frontend/src/components/app/map/controls/ScaleIndicatorComponent.js
@@ -0,0 +1,14 @@
+import React from "react";
+import { TILE_SIZE_IN_METERS, TILE_SIZE_IN_PIXELS } from "../MapConstants";
+import "./ScaleIndicatorComponent.css";
+
+const ScaleIndicatorComponent = ({ scale }) => (
+ <div
+ className="scale-indicator"
+ style={{ width: TILE_SIZE_IN_PIXELS * scale }}
+ >
+ {TILE_SIZE_IN_METERS}m
+ </div>
+);
+
+export default ScaleIndicatorComponent;
diff --git a/frontend/src/components/app/map/controls/ScaleIndicatorComponent.sass b/frontend/src/components/app/map/controls/ScaleIndicatorComponent.sass
new file mode 100644
index 00000000..f2d2b55b
--- /dev/null
+++ b/frontend/src/components/app/map/controls/ScaleIndicatorComponent.sass
@@ -0,0 +1,9 @@
+.scale-indicator
+ position: absolute
+ right: 10px
+ bottom: 10px
+ z-index: 50
+
+ border: solid 2px #212529
+ border-top: none
+ border-left: none
diff --git a/frontend/src/components/app/map/controls/ToolPanelComponent.js b/frontend/src/components/app/map/controls/ToolPanelComponent.js
new file mode 100644
index 00000000..605e9887
--- /dev/null
+++ b/frontend/src/components/app/map/controls/ToolPanelComponent.js
@@ -0,0 +1,13 @@
+import React from "react";
+import ZoomControlContainer from "../../../../containers/app/map/controls/ZoomControlContainer";
+import ExportCanvasComponent from "./ExportCanvasComponent";
+import "./ToolPanelComponent.css";
+
+const ToolPanelComponent = () => (
+ <div className="tool-panel">
+ <ZoomControlContainer />
+ <ExportCanvasComponent />
+ </div>
+);
+
+export default ToolPanelComponent;
diff --git a/frontend/src/components/app/map/controls/ToolPanelComponent.sass b/frontend/src/components/app/map/controls/ToolPanelComponent.sass
new file mode 100644
index 00000000..996712b3
--- /dev/null
+++ b/frontend/src/components/app/map/controls/ToolPanelComponent.sass
@@ -0,0 +1,5 @@
+.tool-panel
+ position: absolute
+ left: 10px
+ bottom: 10px
+ z-index: 50
diff --git a/frontend/src/components/app/map/controls/ZoomControlComponent.js b/frontend/src/components/app/map/controls/ZoomControlComponent.js
new file mode 100644
index 00000000..e1b7491e
--- /dev/null
+++ b/frontend/src/components/app/map/controls/ZoomControlComponent.js
@@ -0,0 +1,24 @@
+import React from "react";
+
+const ZoomControlComponent = ({ zoomInOnCenter }) => {
+ return (
+ <span>
+ <button
+ className="btn btn-default btn-circle btn-sm mr-1"
+ title="Zoom in"
+ onClick={() => zoomInOnCenter(true)}
+ >
+ <span className="fa fa-plus" />
+ </button>
+ <button
+ className="btn btn-default btn-circle btn-sm mr-1"
+ title="Zoom out"
+ onClick={() => zoomInOnCenter(false)}
+ >
+ <span className="fa fa-minus" />
+ </button>
+ </span>
+ );
+};
+
+export default ZoomControlComponent;
diff --git a/frontend/src/components/app/map/elements/Backdrop.js b/frontend/src/components/app/map/elements/Backdrop.js
new file mode 100644
index 00000000..57414463
--- /dev/null
+++ b/frontend/src/components/app/map/elements/Backdrop.js
@@ -0,0 +1,16 @@
+import React from "react";
+import { Rect } from "react-konva";
+import { BACKDROP_COLOR } from "../../../../util/colors";
+import { MAP_SIZE_IN_PIXELS } from "../MapConstants";
+
+const Backdrop = () => (
+ <Rect
+ x={0}
+ y={0}
+ width={MAP_SIZE_IN_PIXELS}
+ height={MAP_SIZE_IN_PIXELS}
+ fill={BACKDROP_COLOR}
+ />
+);
+
+export default Backdrop;
diff --git a/frontend/src/components/app/map/elements/GrayLayer.js b/frontend/src/components/app/map/elements/GrayLayer.js
new file mode 100644
index 00000000..28fadd8a
--- /dev/null
+++ b/frontend/src/components/app/map/elements/GrayLayer.js
@@ -0,0 +1,17 @@
+import React from "react";
+import { Rect } from "react-konva";
+import { GRAYED_OUT_AREA_COLOR } from "../../../../util/colors";
+import { MAP_SIZE_IN_PIXELS } from "../MapConstants";
+
+const GrayLayer = ({ onClick }) => (
+ <Rect
+ x={0}
+ y={0}
+ width={MAP_SIZE_IN_PIXELS}
+ height={MAP_SIZE_IN_PIXELS}
+ fill={GRAYED_OUT_AREA_COLOR}
+ onClick={onClick}
+ />
+);
+
+export default GrayLayer;
diff --git a/frontend/src/components/app/map/elements/HoverTile.js b/frontend/src/components/app/map/elements/HoverTile.js
new file mode 100644
index 00000000..42e6547c
--- /dev/null
+++ b/frontend/src/components/app/map/elements/HoverTile.js
@@ -0,0 +1,30 @@
+import PropTypes from "prop-types";
+import React from "react";
+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 }) => (
+ <Rect
+ x={pixelX}
+ y={pixelY}
+ scaleX={scale}
+ scaleY={scale}
+ width={TILE_SIZE_IN_PIXELS}
+ height={TILE_SIZE_IN_PIXELS}
+ fill={isValid ? ROOM_HOVER_VALID_COLOR : ROOM_HOVER_INVALID_COLOR}
+ onClick={onClick}
+ />
+);
+
+HoverTile.propTypes = {
+ pixelX: PropTypes.number.isRequired,
+ pixelY: PropTypes.number.isRequired,
+ isValid: PropTypes.bool.isRequired,
+ onClick: PropTypes.func.isRequired
+};
+
+export default HoverTile;
diff --git a/frontend/src/components/app/map/elements/ImageComponent.js b/frontend/src/components/app/map/elements/ImageComponent.js
new file mode 100644
index 00000000..cf41ddfe
--- /dev/null
+++ b/frontend/src/components/app/map/elements/ImageComponent.js
@@ -0,0 +1,48 @@
+import PropTypes from "prop-types";
+import React from "react";
+import { Image } from "react-konva";
+
+class ImageComponent extends React.Component {
+ static imageCaches = {};
+ static propTypes = {
+ src: PropTypes.string.isRequired,
+ x: PropTypes.number.isRequired,
+ y: PropTypes.number.isRequired,
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired,
+ opacity: PropTypes.number.isRequired
+ };
+
+ state = {
+ image: null
+ };
+
+ componentDidMount() {
+ if (ImageComponent.imageCaches[this.props.src]) {
+ this.setState({ image: ImageComponent.imageCaches[this.props.src] });
+ return;
+ }
+
+ const image = new window.Image();
+ image.src = this.props.src;
+ image.onload = () => {
+ this.setState({ image });
+ ImageComponent.imageCaches[this.props.src] = image;
+ };
+ }
+
+ render() {
+ return (
+ <Image
+ image={this.state.image}
+ x={this.props.x}
+ y={this.props.y}
+ width={this.props.width}
+ height={this.props.height}
+ opacity={this.props.opacity}
+ />
+ );
+ }
+}
+
+export default ImageComponent;
diff --git a/frontend/src/components/app/map/elements/RackFillBar.js b/frontend/src/components/app/map/elements/RackFillBar.js
new file mode 100644
index 00000000..43701d97
--- /dev/null
+++ b/frontend/src/components/app/map/elements/RackFillBar.js
@@ -0,0 +1,89 @@
+import PropTypes from "prop-types";
+import React from "react";
+import { Group, Rect } from "react-konva";
+import {
+ RACK_ENERGY_BAR_BACKGROUND_COLOR,
+ RACK_ENERGY_BAR_FILL_COLOR,
+ RACK_SPACE_BAR_BACKGROUND_COLOR,
+ RACK_SPACE_BAR_FILL_COLOR
+} from "../../../../util/colors";
+import {
+ OBJECT_BORDER_WIDTH_IN_PIXELS,
+ OBJECT_MARGIN_IN_PIXELS,
+ RACK_FILL_ICON_OPACITY,
+ RACK_FILL_ICON_WIDTH,
+ TILE_SIZE_IN_PIXELS
+} from "../MapConstants";
+import ImageComponent from "./ImageComponent";
+
+const RackFillBar = ({ positionX, positionY, type, fillFraction }) => {
+ const halfOfObjectBorderWidth = OBJECT_BORDER_WIDTH_IN_PIXELS / 2;
+ const x =
+ positionX * TILE_SIZE_IN_PIXELS +
+ OBJECT_MARGIN_IN_PIXELS +
+ (type === "space"
+ ? halfOfObjectBorderWidth
+ : 0.5 * (TILE_SIZE_IN_PIXELS - 2 * OBJECT_MARGIN_IN_PIXELS));
+ const startY =
+ positionY * TILE_SIZE_IN_PIXELS +
+ OBJECT_MARGIN_IN_PIXELS +
+ halfOfObjectBorderWidth;
+ const width =
+ 0.5 * (TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2) -
+ halfOfObjectBorderWidth;
+ const fullHeight =
+ TILE_SIZE_IN_PIXELS -
+ OBJECT_MARGIN_IN_PIXELS * 2 -
+ OBJECT_BORDER_WIDTH_IN_PIXELS;
+
+ const fractionHeight = fillFraction * fullHeight;
+ const fractionY =
+ (positionY + 1) * TILE_SIZE_IN_PIXELS -
+ OBJECT_MARGIN_IN_PIXELS -
+ halfOfObjectBorderWidth -
+ fractionHeight;
+
+ return (
+ <Group>
+ <Rect
+ x={x}
+ y={startY}
+ width={width}
+ height={fullHeight}
+ fill={
+ type === "space"
+ ? RACK_SPACE_BAR_BACKGROUND_COLOR
+ : RACK_ENERGY_BAR_BACKGROUND_COLOR
+ }
+ />
+ <Rect
+ x={x}
+ y={fractionY}
+ width={width}
+ height={fractionHeight}
+ fill={
+ type === "space"
+ ? RACK_SPACE_BAR_FILL_COLOR
+ : RACK_ENERGY_BAR_FILL_COLOR
+ }
+ />
+ <ImageComponent
+ src={"/img/topology/rack-" + type + "-icon.png"}
+ x={x + width * 0.5 - RACK_FILL_ICON_WIDTH * 0.5}
+ y={startY + fullHeight * 0.5 - RACK_FILL_ICON_WIDTH * 0.5}
+ width={RACK_FILL_ICON_WIDTH}
+ height={RACK_FILL_ICON_WIDTH}
+ opacity={RACK_FILL_ICON_OPACITY}
+ />
+ </Group>
+ );
+};
+
+RackFillBar.propTypes = {
+ positionX: PropTypes.number.isRequired,
+ positionY: PropTypes.number.isRequired,
+ type: PropTypes.string.isRequired,
+ fillFraction: PropTypes.number.isRequired
+};
+
+export default RackFillBar;
diff --git a/frontend/src/components/app/map/elements/RoomTile.js b/frontend/src/components/app/map/elements/RoomTile.js
new file mode 100644
index 00000000..71c3bf15
--- /dev/null
+++ b/frontend/src/components/app/map/elements/RoomTile.js
@@ -0,0 +1,20 @@
+import React from "react";
+import { Rect } from "react-konva";
+import Shapes from "../../../../shapes/index";
+import { TILE_SIZE_IN_PIXELS } from "../MapConstants";
+
+const RoomTile = ({ tile, color }) => (
+ <Rect
+ x={tile.positionX * TILE_SIZE_IN_PIXELS}
+ y={tile.positionY * TILE_SIZE_IN_PIXELS}
+ width={TILE_SIZE_IN_PIXELS}
+ height={TILE_SIZE_IN_PIXELS}
+ fill={color}
+ />
+);
+
+RoomTile.propTypes = {
+ tile: Shapes.Tile
+};
+
+export default RoomTile;
diff --git a/frontend/src/components/app/map/elements/TileObject.js b/frontend/src/components/app/map/elements/TileObject.js
new file mode 100644
index 00000000..c1b631db
--- /dev/null
+++ b/frontend/src/components/app/map/elements/TileObject.js
@@ -0,0 +1,29 @@
+import PropTypes from "prop-types";
+import React from "react";
+import { Rect } from "react-konva";
+import { OBJECT_BORDER_COLOR } from "../../../../util/colors";
+import {
+ OBJECT_BORDER_WIDTH_IN_PIXELS,
+ OBJECT_MARGIN_IN_PIXELS,
+ TILE_SIZE_IN_PIXELS
+} from "../MapConstants";
+
+const TileObject = ({ positionX, positionY, color }) => (
+ <Rect
+ x={positionX * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS}
+ y={positionY * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS}
+ width={TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2}
+ height={TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2}
+ fill={color}
+ stroke={OBJECT_BORDER_COLOR}
+ strokeWidth={OBJECT_BORDER_WIDTH_IN_PIXELS}
+ />
+);
+
+TileObject.propTypes = {
+ positionX: PropTypes.number.isRequired,
+ positionY: PropTypes.number.isRequired,
+ color: PropTypes.string.isRequired
+};
+
+export default TileObject;
diff --git a/frontend/src/components/app/map/elements/TilePlusIcon.js b/frontend/src/components/app/map/elements/TilePlusIcon.js
new file mode 100644
index 00000000..06377152
--- /dev/null
+++ b/frontend/src/components/app/map/elements/TilePlusIcon.js
@@ -0,0 +1,52 @@
+import PropTypes from "prop-types";
+import React from "react";
+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 }) => {
+ 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
+ ],
+ [
+ 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
+ ]
+ ];
+ return (
+ <Group>
+ {linePoints.map((points, index) => (
+ <Line
+ key={index}
+ points={points}
+ lineCap="round"
+ stroke={TILE_PLUS_COLOR}
+ strokeWidth={TILE_PLUS_WIDTH_IN_PIXELS * mapScale}
+ listening={false}
+ />
+ ))}
+ </Group>
+ );
+};
+
+TilePlusIcon.propTypes = {
+ pixelX: PropTypes.number,
+ pixelY: PropTypes.number,
+ mapScale: PropTypes.number
+};
+
+export default TilePlusIcon;
diff --git a/frontend/src/components/app/map/elements/WallSegment.js b/frontend/src/components/app/map/elements/WallSegment.js
new file mode 100644
index 00000000..c5011656
--- /dev/null
+++ b/frontend/src/components/app/map/elements/WallSegment.js
@@ -0,0 +1,39 @@
+import React from "react";
+import { Line } from "react-konva";
+import Shapes from "../../../../shapes/index";
+import { WALL_COLOR } from "../../../../util/colors";
+import { TILE_SIZE_IN_PIXELS, WALL_WIDTH_IN_PIXELS } from "../MapConstants";
+
+const WallSegment = ({ wallSegment }) => {
+ let points;
+ if (wallSegment.isHorizontal) {
+ points = [
+ wallSegment.startPosX * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosY * TILE_SIZE_IN_PIXELS,
+ (wallSegment.startPosX + wallSegment.length) * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosY * TILE_SIZE_IN_PIXELS
+ ];
+ } else {
+ points = [
+ wallSegment.startPosX * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosY * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosX * TILE_SIZE_IN_PIXELS,
+ (wallSegment.startPosY + wallSegment.length) * TILE_SIZE_IN_PIXELS
+ ];
+ }
+
+ return (
+ <Line
+ points={points}
+ lineCap="round"
+ stroke={WALL_COLOR}
+ strokeWidth={WALL_WIDTH_IN_PIXELS}
+ />
+ );
+};
+
+WallSegment.propTypes = {
+ wallSegment: Shapes.WallSegment
+};
+
+export default WallSegment;
diff --git a/frontend/src/components/app/map/groups/DatacenterGroup.js b/frontend/src/components/app/map/groups/DatacenterGroup.js
new file mode 100644
index 00000000..51e32db6
--- /dev/null
+++ b/frontend/src/components/app/map/groups/DatacenterGroup.js
@@ -0,0 +1,40 @@
+import React from "react";
+import { Group } from "react-konva";
+import GrayContainer from "../../../../containers/app/map/GrayContainer";
+import RoomContainer from "../../../../containers/app/map/RoomContainer";
+import Shapes from "../../../../shapes/index";
+
+const DatacenterGroup = ({ datacenter, interactionLevel }) => {
+ if (!datacenter) {
+ return <Group />;
+ }
+
+ if (interactionLevel.mode === "BUILDING") {
+ return (
+ <Group>
+ {datacenter.roomIds.map(roomId => (
+ <RoomContainer key={roomId} roomId={roomId} />
+ ))}
+ </Group>
+ );
+ }
+
+ return (
+ <Group>
+ {datacenter.roomIds
+ .filter(roomId => roomId !== interactionLevel.roomId)
+ .map(roomId => <RoomContainer key={roomId} roomId={roomId} />)}
+ {interactionLevel.mode === "ROOM" ? <GrayContainer /> : null}
+ {datacenter.roomIds
+ .filter(roomId => roomId === interactionLevel.roomId)
+ .map(roomId => <RoomContainer key={roomId} roomId={roomId} />)}
+ </Group>
+ );
+};
+
+DatacenterGroup.propTypes = {
+ datacenter: Shapes.Datacenter,
+ interactionLevel: Shapes.InteractionLevel
+};
+
+export default DatacenterGroup;
diff --git a/frontend/src/components/app/map/groups/GridGroup.js b/frontend/src/components/app/map/groups/GridGroup.js
new file mode 100644
index 00000000..bbb1eb68
--- /dev/null
+++ b/frontend/src/components/app/map/groups/GridGroup.js
@@ -0,0 +1,41 @@
+import React from "react";
+import { Group, Line } from "react-konva";
+import { GRID_COLOR } from "../../../../util/colors";
+import {
+ GRID_LINE_WIDTH_IN_PIXELS,
+ MAP_SIZE,
+ MAP_SIZE_IN_PIXELS,
+ TILE_SIZE_IN_PIXELS
+} from "../MapConstants";
+
+const MAP_COORDINATE_ENTRIES = Array.from(new Array(MAP_SIZE), (x, i) => i);
+const HORIZONTAL_POINT_PAIRS = MAP_COORDINATE_ENTRIES.map(index => [
+ 0,
+ index * TILE_SIZE_IN_PIXELS,
+ MAP_SIZE_IN_PIXELS,
+ index * TILE_SIZE_IN_PIXELS
+]);
+const VERTICAL_POINT_PAIRS = MAP_COORDINATE_ENTRIES.map(index => [
+ index * TILE_SIZE_IN_PIXELS,
+ 0,
+ index * TILE_SIZE_IN_PIXELS,
+ MAP_SIZE_IN_PIXELS
+]);
+
+const GridGroup = () => (
+ <Group>
+ {HORIZONTAL_POINT_PAIRS.concat(
+ VERTICAL_POINT_PAIRS
+ ).map((points, index) => (
+ <Line
+ key={index}
+ points={points}
+ stroke={GRID_COLOR}
+ strokeWidth={GRID_LINE_WIDTH_IN_PIXELS}
+ listening={false}
+ />
+ ))}
+ </Group>
+);
+
+export default GridGroup;
diff --git a/frontend/src/components/app/map/groups/RackGroup.js b/frontend/src/components/app/map/groups/RackGroup.js
new file mode 100644
index 00000000..69d6ac10
--- /dev/null
+++ b/frontend/src/components/app/map/groups/RackGroup.js
@@ -0,0 +1,43 @@
+import React from "react";
+import { Group } from "react-konva";
+import RackEnergyFillContainer from "../../../../containers/app/map/RackEnergyFillContainer";
+import RackSpaceFillContainer from "../../../../containers/app/map/RackSpaceFillContainer";
+import Shapes from "../../../../shapes/index";
+import { RACK_BACKGROUND_COLOR } from "../../../../util/colors";
+import { convertLoadToSimulationColor } from "../../../../util/simulation-load";
+import TileObject from "../elements/TileObject";
+
+const RackGroup = ({ tile, inSimulation, rackLoad }) => {
+ let color = RACK_BACKGROUND_COLOR;
+ if (inSimulation && rackLoad >= 0) {
+ color = convertLoadToSimulationColor(rackLoad);
+ }
+
+ return (
+ <Group>
+ <TileObject
+ positionX={tile.positionX}
+ positionY={tile.positionY}
+ color={color}
+ />
+ <Group opacity={inSimulation ? 0.3 : 1}>
+ <RackSpaceFillContainer
+ tileId={tile.id}
+ positionX={tile.positionX}
+ positionY={tile.positionY}
+ />
+ <RackEnergyFillContainer
+ tileId={tile.id}
+ positionX={tile.positionX}
+ positionY={tile.positionY}
+ />
+ </Group>
+ </Group>
+ );
+};
+
+RackGroup.propTypes = {
+ tile: Shapes.Tile
+};
+
+export default RackGroup;
diff --git a/frontend/src/components/app/map/groups/RoomGroup.js b/frontend/src/components/app/map/groups/RoomGroup.js
new file mode 100644
index 00000000..c8f0d3db
--- /dev/null
+++ b/frontend/src/components/app/map/groups/RoomGroup.js
@@ -0,0 +1,56 @@
+import React from "react";
+import { Group } from "react-konva";
+import GrayContainer from "../../../../containers/app/map/GrayContainer";
+import TileContainer from "../../../../containers/app/map/TileContainer";
+import WallContainer from "../../../../containers/app/map/WallContainer";
+import Shapes from "../../../../shapes/index";
+
+const RoomGroup = ({
+ room,
+ interactionLevel,
+ currentRoomInConstruction,
+ onClick
+}) => {
+ if (currentRoomInConstruction === room.id) {
+ return (
+ <Group onClick={onClick}>
+ {room.tileIds.map(tileId => (
+ <TileContainer key={tileId} tileId={tileId} newTile={true} />
+ ))}
+ </Group>
+ );
+ }
+
+ return (
+ <Group onClick={onClick}>
+ {(() => {
+ if (
+ (interactionLevel.mode === "RACK" ||
+ interactionLevel.mode === "MACHINE") &&
+ interactionLevel.roomId === room.id
+ ) {
+ return [
+ room.tileIds
+ .filter(tileId => tileId !== interactionLevel.tileId)
+ .map(tileId => <TileContainer key={tileId} tileId={tileId} />),
+ <GrayContainer key={-1} />,
+ room.tileIds
+ .filter(tileId => tileId === interactionLevel.tileId)
+ .map(tileId => <TileContainer key={tileId} tileId={tileId} />)
+ ];
+ } else {
+ return room.tileIds.map(tileId => (
+ <TileContainer key={tileId} tileId={tileId} />
+ ));
+ }
+ })()}
+ <WallContainer roomId={room.id} />
+ </Group>
+ );
+};
+
+RoomGroup.propTypes = {
+ room: Shapes.Room
+};
+
+export default RoomGroup;
diff --git a/frontend/src/components/app/map/groups/TileGroup.js b/frontend/src/components/app/map/groups/TileGroup.js
new file mode 100644
index 00000000..8f3953d7
--- /dev/null
+++ b/frontend/src/components/app/map/groups/TileGroup.js
@@ -0,0 +1,43 @@
+import PropTypes from "prop-types";
+import React from "react";
+import { Group } from "react-konva";
+import RackContainer from "../../../../containers/app/map/RackContainer";
+import Shapes from "../../../../shapes/index";
+import {
+ ROOM_DEFAULT_COLOR,
+ ROOM_IN_CONSTRUCTION_COLOR
+} from "../../../../util/colors";
+import { convertLoadToSimulationColor } from "../../../../util/simulation-load";
+import RoomTile from "../elements/RoomTile";
+
+const TileGroup = ({ tile, newTile, inSimulation, roomLoad, onClick }) => {
+ let tileObject;
+ switch (tile.objectType) {
+ case "RACK":
+ tileObject = <RackContainer tile={tile} />;
+ break;
+ default:
+ tileObject = null;
+ }
+
+ let color = ROOM_DEFAULT_COLOR;
+ if (newTile) {
+ color = ROOM_IN_CONSTRUCTION_COLOR;
+ } else if (inSimulation && roomLoad >= 0) {
+ color = convertLoadToSimulationColor(roomLoad);
+ }
+
+ return (
+ <Group onClick={() => onClick(tile)}>
+ <RoomTile tile={tile} color={color} />
+ {tileObject}
+ </Group>
+ );
+};
+
+TileGroup.propTypes = {
+ tile: Shapes.Tile,
+ newTile: PropTypes.bool
+};
+
+export default TileGroup;
diff --git a/frontend/src/components/app/map/groups/WallGroup.js b/frontend/src/components/app/map/groups/WallGroup.js
new file mode 100644
index 00000000..43de66e8
--- /dev/null
+++ b/frontend/src/components/app/map/groups/WallGroup.js
@@ -0,0 +1,22 @@
+import PropTypes from "prop-types";
+import React from "react";
+import { Group } from "react-konva";
+import Shapes from "../../../../shapes/index";
+import { deriveWallLocations } from "../../../../util/tile-calculations";
+import WallSegment from "../elements/WallSegment";
+
+const WallGroup = ({ tiles }) => {
+ return (
+ <Group>
+ {deriveWallLocations(tiles).map((wallSegment, index) => (
+ <WallSegment key={index} wallSegment={wallSegment} />
+ ))}
+ </Group>
+ );
+};
+
+WallGroup.propTypes = {
+ tiles: PropTypes.arrayOf(Shapes.Tile).isRequired
+};
+
+export default WallGroup;
diff --git a/frontend/src/components/app/map/layers/HoverLayerComponent.js b/frontend/src/components/app/map/layers/HoverLayerComponent.js
new file mode 100644
index 00000000..c39532f1
--- /dev/null
+++ b/frontend/src/components/app/map/layers/HoverLayerComponent.js
@@ -0,0 +1,85 @@
+import PropTypes from "prop-types";
+import React from "react";
+import { Layer } from "react-konva";
+import HoverTile from "../elements/HoverTile";
+import { TILE_SIZE_IN_PIXELS } from "../MapConstants";
+
+class HoverLayerComponent extends React.Component {
+ static propTypes = {
+ mouseX: PropTypes.number.isRequired,
+ mouseY: PropTypes.number.isRequired,
+ mapPosition: PropTypes.object.isRequired,
+ mapScale: PropTypes.number.isRequired,
+ isEnabled: PropTypes.func.isRequired,
+ onClick: PropTypes.func.isRequired
+ };
+
+ state = {
+ positionX: -1,
+ positionY: -1,
+ validity: false
+ };
+
+ componentDidUpdate() {
+ if (!this.props.isEnabled()) {
+ return;
+ }
+
+ const positionX = Math.floor(
+ (this.props.mouseX - this.props.mapPosition.x) /
+ (this.props.mapScale * TILE_SIZE_IN_PIXELS)
+ );
+ const positionY = Math.floor(
+ (this.props.mouseY - this.props.mapPosition.y) /
+ (this.props.mapScale * TILE_SIZE_IN_PIXELS)
+ );
+
+ if (
+ positionX !== this.state.positionX ||
+ positionY !== this.state.positionY
+ ) {
+ this.setState({
+ positionX,
+ positionY,
+ validity: this.props.isValid(positionX, positionY)
+ });
+ }
+ }
+
+ render() {
+ if (!this.props.isEnabled()) {
+ return <Layer />;
+ }
+
+ const pixelX =
+ this.props.mapScale * this.state.positionX * TILE_SIZE_IN_PIXELS +
+ this.props.mapPosition.x;
+ const pixelY =
+ this.props.mapScale * this.state.positionY * TILE_SIZE_IN_PIXELS +
+ this.props.mapPosition.y;
+
+ return (
+ <Layer opacity={0.6}>
+ <HoverTile
+ pixelX={pixelX}
+ pixelY={pixelY}
+ scale={this.props.mapScale}
+ isValid={this.state.validity}
+ onClick={() =>
+ this.state.validity
+ ? this.props.onClick(this.state.positionX, this.state.positionY)
+ : undefined}
+ />
+ {this.props.children
+ ? React.cloneElement(this.props.children, {
+ pixelX,
+ pixelY,
+ scale: this.props.mapScale
+ })
+ : undefined}
+ </Layer>
+ );
+ }
+}
+
+export default HoverLayerComponent;
diff --git a/frontend/src/components/app/map/layers/MapLayerComponent.js b/frontend/src/components/app/map/layers/MapLayerComponent.js
new file mode 100644
index 00000000..6ad3cb88
--- /dev/null
+++ b/frontend/src/components/app/map/layers/MapLayerComponent.js
@@ -0,0 +1,22 @@
+import React from "react";
+import { Group, Layer } from "react-konva";
+import DatacenterContainer from "../../../../containers/app/map/DatacenterContainer";
+import Backdrop from "../elements/Backdrop";
+import GridGroup from "../groups/GridGroup";
+
+const MapLayerComponent = ({ mapPosition, mapScale }) => (
+ <Layer>
+ <Group
+ x={mapPosition.x}
+ y={mapPosition.y}
+ scaleX={mapScale}
+ scaleY={mapScale}
+ >
+ <Backdrop />
+ <DatacenterContainer />
+ <GridGroup />
+ </Group>
+ </Layer>
+);
+
+export default MapLayerComponent;
diff --git a/frontend/src/components/app/map/layers/ObjectHoverLayerComponent.js b/frontend/src/components/app/map/layers/ObjectHoverLayerComponent.js
new file mode 100644
index 00000000..e7342d3c
--- /dev/null
+++ b/frontend/src/components/app/map/layers/ObjectHoverLayerComponent.js
@@ -0,0 +1,11 @@
+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/frontend/src/components/app/map/layers/RoomHoverLayerComponent.js b/frontend/src/components/app/map/layers/RoomHoverLayerComponent.js
new file mode 100644
index 00000000..feea5ae5
--- /dev/null
+++ b/frontend/src/components/app/map/layers/RoomHoverLayerComponent.js
@@ -0,0 +1,6 @@
+import React from "react";
+import HoverLayerComponent from "./HoverLayerComponent";
+
+const RoomHoverLayerComponent = props => <HoverLayerComponent {...props} />;
+
+export default RoomHoverLayerComponent;