summaryrefslogtreecommitdiff
path: root/src/components/app/map
diff options
context:
space:
mode:
authorGeorgios Andreadis <g.andreadis@student.tudelft.nl>2017-09-22 21:20:54 +0200
committerGeorgios Andreadis <g.andreadis@student.tudelft.nl>2017-09-23 10:06:18 +0200
commitbf7708f658cc6299a3b775afe24459b5a808c54d (patch)
tree227520267968759e2a2f1e29e6f3edfeb4e3cf8a /src/components/app/map
parente722cf117d0e3ebac20237f96764fb08cab49a62 (diff)
Restructure component and container directories
Diffstat (limited to 'src/components/app/map')
-rw-r--r--src/components/app/map/LoadingScreen.js11
-rw-r--r--src/components/app/map/MapConstants.js28
-rw-r--r--src/components/app/map/MapStageComponent.js135
-rw-r--r--src/components/app/map/controls/ExportCanvasComponent.js13
-rw-r--r--src/components/app/map/controls/ScaleIndicatorComponent.js14
-rw-r--r--src/components/app/map/controls/ScaleIndicatorComponent.sass9
-rw-r--r--src/components/app/map/controls/ToolPanelComponent.js13
-rw-r--r--src/components/app/map/controls/ToolPanelComponent.sass5
-rw-r--r--src/components/app/map/controls/ZoomControlComponent.js31
-rw-r--r--src/components/app/map/elements/Backdrop.js16
-rw-r--r--src/components/app/map/elements/GrayLayer.js17
-rw-r--r--src/components/app/map/elements/HoverTile.js27
-rw-r--r--src/components/app/map/elements/ImageComponent.js48
-rw-r--r--src/components/app/map/elements/RackFillBar.js67
-rw-r--r--src/components/app/map/elements/RoomTile.js20
-rw-r--r--src/components/app/map/elements/TileObject.js25
-rw-r--r--src/components/app/map/elements/TilePlusIcon.js44
-rw-r--r--src/components/app/map/elements/WallSegment.js39
-rw-r--r--src/components/app/map/groups/DatacenterGroup.js42
-rw-r--r--src/components/app/map/groups/GridGroup.js30
-rw-r--r--src/components/app/map/groups/RackGroup.js34
-rw-r--r--src/components/app/map/groups/RoomGroup.js48
-rw-r--r--src/components/app/map/groups/TileGroup.js42
-rw-r--r--src/components/app/map/groups/WallGroup.js22
-rw-r--r--src/components/app/map/layers/HoverLayerComponent.js63
-rw-r--r--src/components/app/map/layers/MapLayerComponent.js17
-rw-r--r--src/components/app/map/layers/ObjectHoverLayerComponent.js11
-rw-r--r--src/components/app/map/layers/RoomHoverLayerComponent.js8
28 files changed, 879 insertions, 0 deletions
diff --git a/src/components/app/map/LoadingScreen.js b/src/components/app/map/LoadingScreen.js
new file mode 100644
index 00000000..3d5753e2
--- /dev/null
+++ b/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/src/components/app/map/MapConstants.js b/src/components/app/map/MapConstants.js
new file mode 100644
index 00000000..a0166d15
--- /dev/null
+++ b/src/components/app/map/MapConstants.js
@@ -0,0 +1,28 @@
+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/src/components/app/map/MapStageComponent.js b/src/components/app/map/MapStageComponent.js
new file mode 100644
index 00000000..271ae64f
--- /dev/null
+++ b/src/components/app/map/MapStageComponent.js
@@ -0,0 +1,135 @@
+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_MAX_SCALE,
+ MAP_MIN_SCALE,
+ MAP_MOVE_PIXELS_PER_EVENT,
+ MAP_SCALE_PER_EVENT,
+ MAP_SIZE_IN_PIXELS
+} from "./MapConstants";
+
+class MapStageComponent extends React.Component {
+ state = {
+ mouseX: 0,
+ mouseY: 0
+ };
+
+ constructor() {
+ super();
+
+ 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 canvasData = this.stage.getStage().toDataURL();
+ const newWindow = window.open('about:blank', 'OpenDC Canvas Export');
+ newWindow.document.write("<img src='" + canvasData + "' alt='Canvas Image Export'/>");
+ newWindow.document.title = "OpenDC Canvas Export";
+ }
+ }
+
+ 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();
+ const mousePointsTo = {
+ x: this.state.mouseX / this.props.mapScale - this.props.mapPosition.x / this.props.mapScale,
+ y: this.state.mouseY / this.props.mapScale - this.props.mapPosition.y / this.props.mapScale,
+ };
+ const newScale = e.deltaY < 0 ? this.props.mapScale * MAP_SCALE_PER_EVENT : this.props.mapScale / MAP_SCALE_PER_EVENT;
+ const boundedScale = Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE);
+
+ const newX = -(mousePointsTo.x - this.state.mouseX / boundedScale) * boundedScale;
+ const newY = -(mousePointsTo.y - this.state.mouseY / boundedScale) * boundedScale;
+
+ this.setPositionWithBoundsCheck(newX, newY);
+ this.props.setMapScale(boundedScale);
+ }
+
+ 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.setPositionWithBoundsCheck(this.props.mapPosition.x + deltaX, this.props.mapPosition.y + deltaY);
+ }
+
+ setPositionWithBoundsCheck(newX, newY) {
+ const scaledMapSize = MAP_SIZE_IN_PIXELS * this.props.mapScale;
+ const updatedX = newX > 0 ? 0 :
+ (newX < -scaledMapSize + this.props.mapDimensions.width
+ ? -scaledMapSize + this.props.mapDimensions.width : newX);
+ const updatedY = newY > 0 ? 0 :
+ (newY < -scaledMapSize + this.props.mapDimensions.height
+ ? -scaledMapSize + this.props.mapDimensions.height : newY);
+
+ this.props.setMapPosition(updatedX, updatedY);
+ }
+
+ 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)}
+ >
+ <MapLayer/>
+ <RoomHoverLayer
+ mouseX={this.state.mouseX}
+ mouseY={this.state.mouseY}
+ />
+ <ObjectHoverLayer
+ mouseX={this.state.mouseX}
+ mouseY={this.state.mouseY}
+ />
+ </Stage>
+ </Shortcuts>
+ )
+ }
+}
+
+export default MapStageComponent;
diff --git a/src/components/app/map/controls/ExportCanvasComponent.js b/src/components/app/map/controls/ExportCanvasComponent.js
new file mode 100644
index 00000000..2f044ffe
--- /dev/null
+++ b/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/src/components/app/map/controls/ScaleIndicatorComponent.js b/src/components/app/map/controls/ScaleIndicatorComponent.js
new file mode 100644
index 00000000..fd9483b5
--- /dev/null
+++ b/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/src/components/app/map/controls/ScaleIndicatorComponent.sass b/src/components/app/map/controls/ScaleIndicatorComponent.sass
new file mode 100644
index 00000000..f2d2b55b
--- /dev/null
+++ b/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/src/components/app/map/controls/ToolPanelComponent.js b/src/components/app/map/controls/ToolPanelComponent.js
new file mode 100644
index 00000000..a065358a
--- /dev/null
+++ b/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/src/components/app/map/controls/ToolPanelComponent.sass b/src/components/app/map/controls/ToolPanelComponent.sass
new file mode 100644
index 00000000..996712b3
--- /dev/null
+++ b/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/src/components/app/map/controls/ZoomControlComponent.js b/src/components/app/map/controls/ZoomControlComponent.js
new file mode 100644
index 00000000..c5628d16
--- /dev/null
+++ b/src/components/app/map/controls/ZoomControlComponent.js
@@ -0,0 +1,31 @@
+import React from "react";
+import {MAP_MAX_SCALE, MAP_MIN_SCALE, MAP_SCALE_PER_EVENT} from "../MapConstants";
+
+const ZoomControlComponent = ({mapScale, setMapScale}) => {
+ const zoom = (out) => {
+ const newScale = out ? mapScale / MAP_SCALE_PER_EVENT : mapScale * MAP_SCALE_PER_EVENT;
+ const boundedScale = Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE);
+ setMapScale(boundedScale);
+ };
+
+ return (
+ <span>
+ <button
+ className="btn btn-default btn-circle btn-sm mr-1"
+ title="Zoom in"
+ onClick={() => zoom(false)}
+ >
+ <span className="fa fa-plus"/>
+ </button>
+ <button
+ className="btn btn-default btn-circle btn-sm mr-1"
+ title="Zoom out"
+ onClick={() => zoom(true)}
+ >
+ <span className="fa fa-minus"/>
+ </button>
+ </span>
+ );
+};
+
+export default ZoomControlComponent;
diff --git a/src/components/app/map/elements/Backdrop.js b/src/components/app/map/elements/Backdrop.js
new file mode 100644
index 00000000..9c01df63
--- /dev/null
+++ b/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/src/components/app/map/elements/GrayLayer.js b/src/components/app/map/elements/GrayLayer.js
new file mode 100644
index 00000000..c5994d06
--- /dev/null
+++ b/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/src/components/app/map/elements/HoverTile.js b/src/components/app/map/elements/HoverTile.js
new file mode 100644
index 00000000..fc12cbdd
--- /dev/null
+++ b/src/components/app/map/elements/HoverTile.js
@@ -0,0 +1,27 @@
+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/src/components/app/map/elements/ImageComponent.js b/src/components/app/map/elements/ImageComponent.js
new file mode 100644
index 00000000..486296ea
--- /dev/null
+++ b/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/src/components/app/map/elements/RackFillBar.js b/src/components/app/map/elements/RackFillBar.js
new file mode 100644
index 00000000..3a8a1137
--- /dev/null
+++ b/src/components/app/map/elements/RackFillBar.js
@@ -0,0 +1,67 @@
+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/src/components/app/map/elements/RoomTile.js b/src/components/app/map/elements/RoomTile.js
new file mode 100644
index 00000000..11948a7a
--- /dev/null
+++ b/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/src/components/app/map/elements/TileObject.js b/src/components/app/map/elements/TileObject.js
new file mode 100644
index 00000000..73bfddba
--- /dev/null
+++ b/src/components/app/map/elements/TileObject.js
@@ -0,0 +1,25 @@
+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/src/components/app/map/elements/TilePlusIcon.js b/src/components/app/map/elements/TilePlusIcon.js
new file mode 100644
index 00000000..b96bf0f5
--- /dev/null
+++ b/src/components/app/map/elements/TilePlusIcon.js
@@ -0,0 +1,44 @@
+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/src/components/app/map/elements/WallSegment.js b/src/components/app/map/elements/WallSegment.js
new file mode 100644
index 00000000..14efd3fc
--- /dev/null
+++ b/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/src/components/app/map/groups/DatacenterGroup.js b/src/components/app/map/groups/DatacenterGroup.js
new file mode 100644
index 00000000..1c978360
--- /dev/null
+++ b/src/components/app/map/groups/DatacenterGroup.js
@@ -0,0 +1,42 @@
+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/src/components/app/map/groups/GridGroup.js b/src/components/app/map/groups/GridGroup.js
new file mode 100644
index 00000000..b3c6e1d5
--- /dev/null
+++ b/src/components/app/map/groups/GridGroup.js
@@ -0,0 +1,30 @@
+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/src/components/app/map/groups/RackGroup.js b/src/components/app/map/groups/RackGroup.js
new file mode 100644
index 00000000..233d0c20
--- /dev/null
+++ b/src/components/app/map/groups/RackGroup.js
@@ -0,0 +1,34 @@
+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) {
+ color = convertLoadToSimulationColor(rackLoad);
+ }
+
+ return (
+ <Group>
+ <TileObject positionX={tile.positionX} positionY={tile.positionY} color={color}/>
+ {inSimulation ?
+ undefined :
+ <Group>
+ <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/src/components/app/map/groups/RoomGroup.js b/src/components/app/map/groups/RoomGroup.js
new file mode 100644
index 00000000..18a6bd84
--- /dev/null
+++ b/src/components/app/map/groups/RoomGroup.js
@@ -0,0 +1,48 @@
+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/src/components/app/map/groups/TileGroup.js b/src/components/app/map/groups/TileGroup.js
new file mode 100644
index 00000000..c41e78a4
--- /dev/null
+++ b/src/components/app/map/groups/TileGroup.js
@@ -0,0 +1,42 @@
+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) {
+ 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/src/components/app/map/groups/WallGroup.js b/src/components/app/map/groups/WallGroup.js
new file mode 100644
index 00000000..6de22523
--- /dev/null
+++ b/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/src/components/app/map/layers/HoverLayerComponent.js b/src/components/app/map/layers/HoverLayerComponent.js
new file mode 100644
index 00000000..aa2e8313
--- /dev/null
+++ b/src/components/app/map/layers/HoverLayerComponent.js
@@ -0,0 +1,63 @@
+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/src/components/app/map/layers/MapLayerComponent.js b/src/components/app/map/layers/MapLayerComponent.js
new file mode 100644
index 00000000..c969249c
--- /dev/null
+++ b/src/components/app/map/layers/MapLayerComponent.js
@@ -0,0 +1,17 @@
+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/src/components/app/map/layers/ObjectHoverLayerComponent.js b/src/components/app/map/layers/ObjectHoverLayerComponent.js
new file mode 100644
index 00000000..aa79f8c3
--- /dev/null
+++ b/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/src/components/app/map/layers/RoomHoverLayerComponent.js b/src/components/app/map/layers/RoomHoverLayerComponent.js
new file mode 100644
index 00000000..2133c8d8
--- /dev/null
+++ b/src/components/app/map/layers/RoomHoverLayerComponent.js
@@ -0,0 +1,8 @@
+import React from 'react';
+import HoverLayerComponent from "./HoverLayerComponent";
+
+const RoomHoverLayerComponent = (props) => (
+ <HoverLayerComponent {...props}/>
+);
+
+export default RoomHoverLayerComponent;