SRP
This commit is contained in:
109
README.md
Normal file
109
README.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# Community Energy Simulation
|
||||||
|
|
||||||
|
A 3D Digital Twin application for simulating and visualizing community energy systems. This application provides an interactive 3D interface to monitor energy consumption and generation across various building types in a community.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 3D Digital Twin
|
||||||
|
- **Interactive Scene**: Explore the community with full camera controls (pan, rotate, zoom).
|
||||||
|
- **Building Selection**: Click on buildings to view detailed information and highlight them in the scene.
|
||||||
|
- **Visual Feedback**: Emissive highlighting and selection rings for selected objects.
|
||||||
|
|
||||||
|
### Simulation Modes
|
||||||
|
- **Simulated Mode**: Runs an internal simulation with a day/night cycle, calculating energy loads based on time of day (solar generation curves, evening consumption peaks).
|
||||||
|
- **Live Mode**: Connects to an external backend (via Socket.io) to visualize real-time data streaming from Redis Pub/Sub.
|
||||||
|
|
||||||
|
### Dynamic Configuration
|
||||||
|
- **Add Buildings**: Dynamically add new buildings to the scene by specifying ID, Type (house, factory, etc.), and CSV Data path.
|
||||||
|
- **Adjustable Settings**:
|
||||||
|
- **Simulation Speed**: Control how fast time passes in simulated mode.
|
||||||
|
- **Time of Day**: Visual day/night cycle.
|
||||||
|
- **Brightness & Zoom**: Customize the visual experience.
|
||||||
|
- **Data Origin**: Configure a base URL for loading remote CSV datasets.
|
||||||
|
- **Backend URL**: Configure the Socket.io server connection for Live Mode.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Frontend Framework**: Vue.js 3 + Vite
|
||||||
|
- **Language**: TypeScript
|
||||||
|
- **3D Library**: Three.js
|
||||||
|
- **State Management**: Vue Composition API (Reactivity)
|
||||||
|
- **Data Handling**: PapaParse (CSV), Socket.io-client (Real-time)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js (v18 or higher)
|
||||||
|
- npm or yarn
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Navigate to the web application directory:
|
||||||
|
```bash
|
||||||
|
cd web-app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Locally
|
||||||
|
|
||||||
|
Start the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
Build the application for deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The output will be in the `dist` directory.
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
A `Dockerfile` and `docker.sh` script are provided for containerization.
|
||||||
|
|
||||||
|
To build and push the image (requires Docker login):
|
||||||
|
```bash
|
||||||
|
./docker.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Or run locally:
|
||||||
|
```bash
|
||||||
|
docker build -t community-simulation .
|
||||||
|
docker run -p 8080:8080 community-simulation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Connecting to Live Data
|
||||||
|
1. Ensure your backend service (Redis + Socket.io bridge) is running.
|
||||||
|
2. Open the application settings (Gear icon).
|
||||||
|
3. Select **Live (Real Time)** mode.
|
||||||
|
4. Enter your **Backend URL** (default: `http://localhost:8000`).
|
||||||
|
5. Save settings. The app will connect and listen for `simulation:update` events.
|
||||||
|
|
||||||
|
### Adding Custom Buildings
|
||||||
|
1. Click the **+ Add Building** button.
|
||||||
|
2. Enter a unique **ID** (e.g., "H99").
|
||||||
|
3. Select a **Type** (Determines the 3D model used).
|
||||||
|
4. Provide a **CSV Path** relative to the `public/` folder or your configured Data Origin (e.g., `data/my-building.csv`).
|
||||||
|
5. Click **Add**.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- `web-app/`: Main Vue.js application source code.
|
||||||
|
- `src/components/`: UI components (Modals, Overlay).
|
||||||
|
- `src/composables/`: Game logic and state management (`useSimulation`, `useCityObjects`, etc.).
|
||||||
|
- `public/`: Static assets (3D models, textures, CSV data).
|
||||||
|
- `js/`: Legacy/Prototype JavaScript files.
|
||||||
@@ -60,6 +60,19 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group" v-if="simulationModeInput === 'live'">
|
||||||
|
<label>Backend URL (Live Mode)</label>
|
||||||
|
<div class="description">
|
||||||
|
WebSocket server address for real-time updates.
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="backendUrlInput"
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g., http://localhost:8000"
|
||||||
|
class="input-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label>Data Origin (Base URL)</label>
|
<label>Data Origin (Base URL)</label>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
@@ -92,8 +105,9 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
const { state, setDataBaseUrl, setBrightness, setSimulationMode, setMaxZoom, setZoomSensitivity } = useSimulation();
|
const { state, setDataBaseUrl, setBackendUrl, setBrightness, setSimulationMode, setMaxZoom, setZoomSensitivity } = useSimulation();
|
||||||
const baseUrlInput = ref('');
|
const baseUrlInput = ref('');
|
||||||
|
const backendUrlInput = ref('');
|
||||||
const brightnessInput = ref(1.0);
|
const brightnessInput = ref(1.0);
|
||||||
const simulationModeInput = ref<'simulated' | 'live'>('simulated');
|
const simulationModeInput = ref<'simulated' | 'live'>('simulated');
|
||||||
const maxZoomInput = ref(120);
|
const maxZoomInput = ref(120);
|
||||||
@@ -103,6 +117,7 @@ const zoomSensitivityInput = ref(5);
|
|||||||
watch(() => props.isOpen, (newVal) => {
|
watch(() => props.isOpen, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
baseUrlInput.value = state.value.dataBaseUrl || '';
|
baseUrlInput.value = state.value.dataBaseUrl || '';
|
||||||
|
backendUrlInput.value = state.value.backendUrl || 'http://localhost:8000';
|
||||||
brightnessInput.value = state.value.brightness ?? 1.0;
|
brightnessInput.value = state.value.brightness ?? 1.0;
|
||||||
simulationModeInput.value = state.value.simulationMode ?? 'simulated';
|
simulationModeInput.value = state.value.simulationMode ?? 'simulated';
|
||||||
maxZoomInput.value = state.value.maxZoom ?? 120;
|
maxZoomInput.value = state.value.maxZoom ?? 120;
|
||||||
@@ -116,6 +131,7 @@ const close = () => {
|
|||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
setDataBaseUrl(baseUrlInput.value);
|
setDataBaseUrl(baseUrlInput.value);
|
||||||
|
setBackendUrl(backendUrlInput.value);
|
||||||
setBrightness(Number(brightnessInput.value));
|
setBrightness(Number(brightnessInput.value));
|
||||||
setSimulationMode(simulationModeInput.value);
|
setSimulationMode(simulationModeInput.value);
|
||||||
setMaxZoom(Number(maxZoomInput.value));
|
setMaxZoom(Number(maxZoomInput.value));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import Papa from 'papaparse';
|
import Papa from 'papaparse';
|
||||||
|
import { io, type Socket } from 'socket.io-client';
|
||||||
|
|
||||||
interface SimulationState {
|
interface SimulationState {
|
||||||
currentTime: number; // 0 to 24 (hours)
|
currentTime: number; // 0 to 24 (hours)
|
||||||
@@ -10,6 +11,7 @@ interface SimulationState {
|
|||||||
totalConsumption: number;
|
totalConsumption: number;
|
||||||
totalGeneration: number;
|
totalGeneration: number;
|
||||||
dataBaseUrl: string;
|
dataBaseUrl: string;
|
||||||
|
backendUrl: string;
|
||||||
brightness: number;
|
brightness: number;
|
||||||
simulationMode: 'simulated' | 'live';
|
simulationMode: 'simulated' | 'live';
|
||||||
maxZoom: number;
|
maxZoom: number;
|
||||||
@@ -60,6 +62,7 @@ const state = ref<SimulationState>({
|
|||||||
totalConsumption: 0,
|
totalConsumption: 0,
|
||||||
totalGeneration: 0,
|
totalGeneration: 0,
|
||||||
dataBaseUrl: '',
|
dataBaseUrl: '',
|
||||||
|
backendUrl: 'http://localhost:8000',
|
||||||
brightness: 1.0,
|
brightness: 1.0,
|
||||||
simulationMode: 'simulated',
|
simulationMode: 'simulated',
|
||||||
maxZoom: 120,
|
maxZoom: 120,
|
||||||
@@ -83,6 +86,8 @@ const state = ref<SimulationState>({
|
|||||||
});
|
});
|
||||||
const selectedBuilding = ref<BuildingData | null>(null);
|
const selectedBuilding = ref<BuildingData | null>(null);
|
||||||
|
|
||||||
|
let socket: Socket | null = null;
|
||||||
|
|
||||||
export const useSimulation = () => {
|
export const useSimulation = () => {
|
||||||
|
|
||||||
const togglePlay = () => {
|
const togglePlay = () => {
|
||||||
@@ -97,10 +102,50 @@ export const useSimulation = () => {
|
|||||||
state.value.brightness = val;
|
state.value.brightness = val;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initSocket = () => {
|
||||||
|
if (socket) return;
|
||||||
|
|
||||||
|
console.log(`Connecting to backend: ${state.value.backendUrl}`);
|
||||||
|
socket = io(state.value.backendUrl);
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
console.log('Socket connected');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('Socket disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('simulation:update', (data: any) => {
|
||||||
|
// Expected data format: { totalConsumption: number, totalGeneration: number, buildings: { [id]: { consumption: number, generation: number } } }
|
||||||
|
// OR individual updates. Let's assume a snapshot for now or aggregate.
|
||||||
|
// If data contains totals:
|
||||||
|
if (data.totalConsumption !== undefined) state.value.totalConsumption = data.totalConsumption;
|
||||||
|
if (data.totalGeneration !== undefined) state.value.totalGeneration = data.totalGeneration;
|
||||||
|
|
||||||
|
// Update selected building if present
|
||||||
|
if (selectedBuilding.value && data.buildings && data.buildings[selectedBuilding.value.id]) {
|
||||||
|
const bUpdate = data.buildings[selectedBuilding.value.id];
|
||||||
|
selectedBuilding.value.consumption = bUpdate.consumption;
|
||||||
|
selectedBuilding.value.generation = bUpdate.generation;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnectSocket = () => {
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect();
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setSimulationMode = (mode: 'simulated' | 'live') => {
|
const setSimulationMode = (mode: 'simulated' | 'live') => {
|
||||||
state.value.simulationMode = mode;
|
state.value.simulationMode = mode;
|
||||||
if (mode === 'live') {
|
if (mode === 'live') {
|
||||||
state.value.isPlaying = true; // Auto-play in live? Or just simple update.
|
state.value.isPlaying = true; // Auto-play in live? Or just simple update.
|
||||||
|
initSocket();
|
||||||
|
} else {
|
||||||
|
disconnectSocket();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,6 +161,15 @@ export const useSimulation = () => {
|
|||||||
state.value.dataBaseUrl = url;
|
state.value.dataBaseUrl = url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setBackendUrl = (url: string) => {
|
||||||
|
state.value.backendUrl = url;
|
||||||
|
// Reconnect if live
|
||||||
|
if (state.value.simulationMode === 'live' && socket) {
|
||||||
|
disconnectSocket();
|
||||||
|
initSocket();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadBuildingData = (_buildingId: string, csvPath: string) => {
|
const loadBuildingData = (_buildingId: string, csvPath: string) => {
|
||||||
// Construct full URL
|
// Construct full URL
|
||||||
let fullPath = csvPath;
|
let fullPath = csvPath;
|
||||||
@@ -156,34 +210,34 @@ export const useSimulation = () => {
|
|||||||
state.value.currentTime = 0;
|
state.value.currentTime = 0;
|
||||||
state.value.day++;
|
state.value.day++;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- Simulated Community Energy Logic ---
|
// --- Simulated Community Energy Logic ---
|
||||||
const time = state.value.currentTime;
|
const time = state.value.currentTime;
|
||||||
|
|
||||||
// Consumption Curve: Base load + Evening Peak (18-22)
|
// Consumption Curve: Base load + Evening Peak (18-22)
|
||||||
// Base Community Load ~4000kW
|
// Base Community Load ~4000kW
|
||||||
let load = 4000;
|
let load = 4000;
|
||||||
// Peak
|
// Peak
|
||||||
if (time > 17 && time < 23) {
|
if (time > 17 && time < 23) {
|
||||||
load += 2000 * Math.sin(((time - 17) / 6) * Math.PI);
|
load += 2000 * Math.sin(((time - 17) / 6) * Math.PI);
|
||||||
}
|
}
|
||||||
// Morning spike
|
// Morning spike
|
||||||
if (time > 6 && time < 9) {
|
if (time > 6 && time < 9) {
|
||||||
load += 1000 * Math.sin(((time - 6) / 3) * Math.PI);
|
load += 1000 * Math.sin(((time - 6) / 3) * Math.PI);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generation Curve: Based on simplified Sun Elevation
|
// Generation Curve: Based on simplified Sun Elevation
|
||||||
// Simple Parabola from 6am to 6pm
|
// Simple Parabola from 6am to 6pm
|
||||||
let gen = 0;
|
let gen = 0;
|
||||||
if (time > 6 && time < 18) {
|
if (time > 6 && time < 18) {
|
||||||
// Peak at 12
|
// Peak at 12
|
||||||
const sunFactor = Math.sin(((time - 6) / 12) * Math.PI);
|
const sunFactor = Math.sin(((time - 6) / 12) * Math.PI);
|
||||||
gen = 3500 * sunFactor; // Max 3500kW generation
|
gen = 3500 * sunFactor; // Max 3500kW generation
|
||||||
}
|
}
|
||||||
|
|
||||||
state.value.totalConsumption = Math.floor(load);
|
state.value.totalConsumption = Math.floor(load);
|
||||||
state.value.totalGeneration = Math.floor(gen);
|
state.value.totalGeneration = Math.floor(gen);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addBuilding = (def: BuildingDefinition) => {
|
const addBuilding = (def: BuildingDefinition) => {
|
||||||
@@ -198,6 +252,7 @@ export const useSimulation = () => {
|
|||||||
loadBuildingData,
|
loadBuildingData,
|
||||||
updateTime,
|
updateTime,
|
||||||
setDataBaseUrl,
|
setDataBaseUrl,
|
||||||
|
setBackendUrl,
|
||||||
setBrightness,
|
setBrightness,
|
||||||
setSimulationMode,
|
setSimulationMode,
|
||||||
setMaxZoom,
|
setMaxZoom,
|
||||||
|
|||||||
Reference in New Issue
Block a user