Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
<script type="module" src="/src/app/index.jsx"></script>
</body>
</html>
101 changes: 101 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Source Directory Structure

This directory contains the OpenPV application source code, organized by function and feature.

## Directory Overview

### 📱 **app/**

Application core - entry point, routing, and root component.

- `index.jsx` - Application entry point
- `App.jsx` - Root component with layout and meta tags
- `router.jsx` - Route configuration

### 📄 **pages/**

Route-level page components. Each page has its own folder.

- `Map/` - Main map view (root `/`)
- `Simulation/` - Simulation results page
- `About/`, `Impressum/`, `Datenschutz/` - Information pages
- `NotFound/` - 404 page

### 🎯 **features/**

Domain-specific features with their own components, logic, and utilities.

- **three-viewer/** - 3D visualization

- `components/` - Scene, Overlay, Terrain, etc.
- `meshes/` - Building, vegetation, PV system meshes
- `controls/` - Map controls, drawing tools
- `dialogs/` - Options, notifications, legends
- `context/` - Scene state management
- `utils/` - Color mapping utilities

- **map/** - Map functionality

- `components/` - Map popup, search field

- **simulation/** - PV simulation engine
- `core/` - Calculation logic (main, preprocessing, elevation, etc.)
- `components/` - Savings calculation UI

### 🧩 **components/**

Shared, reusable components used across the app.

- `layout/` - Navigation, Footer, AppLayout, LoadingBar, WelcomeMessage
- `ui/` - Chakra UI component wrappers
- `errors/` - Error display components

### ⚙️ **lib/**

Third-party library configurations.

- `i18n.js` - Internationalization setup

### 🛠️ **utils/**

Shared utility functions.

- `device.js` - Device detection utilities

### 📊 **constants/**

Application-wide constants.

- `colors.js` - Color definitions
- `licenses.js` - Data attribution and licenses

### 🎨 **assets/**

Static assets.

- `styles/` - Global CSS

## Import Patterns

All imports use the `@/` alias for clean, absolute imports:

```javascript
// Good
import { Scene } from '@/features/three-viewer/components/Scene'
import { Navigation } from '@/components/layout/Navigation'
import { colors } from '@/constants/colors'

// Avoid relative imports for cross-directory references
import Scene from '../../../features/three-viewer/components/Scene'
```

## Feature Modules

Features are self-contained with their own:

- Components
- Business logic
- Context/state
- Utilities

Each feature exports a public API via `index.js` for use by other parts of the app.
50 changes: 8 additions & 42 deletions src/Main.jsx → src/app/App.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Box } from '@chakra-ui/react'
import PropTypes from 'prop-types'
import React from 'react'
import { Helmet, HelmetProvider } from 'react-helmet-async'
import { useTranslation } from 'react-i18next'
import AppLayout from '@/components/layout/AppLayout'
import Navigation from '@/components/layout/Navigation'

import Navigation from './components/Template/Navigation'

const Main = (props) => {
const App = (props) => {
const { t } = useTranslation()
return (
<HelmetProvider>
Expand All @@ -19,15 +18,15 @@ const Main = (props) => {
<meta name='description' content={props.description} />
<meta name='Beschreibung' content={props.description} />
</Helmet>
<Layout>
<AppLayout>
<Navigation />
{props.children}
</Layout>
</AppLayout>
</HelmetProvider>
)
}

Main.propTypes = {
App.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
Expand All @@ -37,44 +36,11 @@ Main.propTypes = {
description: PropTypes.string,
}

Main.defaultProps = {
App.defaultProps = {
children: null,
fullPage: false,
title: null,
description: 'Ermittle das Potential für eine Solaranlage.',
}

export default Main

const Layout = ({ children }) => {
return (
<Box
as='div'
display='flex'
margin='0'
maxWidth='100%'
opacity={1}
padding={0}
width='100vw'
height='100vh'
position='fixed'
left={0}
top={0}
flexDirection='column'
>
<Box
display='flex'
flexDirection='column'
justifyContent='space-between'
minWidth={0}
minHeight={0}
overflow='hidden'
flexGrow={1}
width='100%'
height='100%'
>
{children}
</Box>
</Box>
)
}
export default App
31 changes: 31 additions & 0 deletions src/app/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Provider } from '@/components/ui/provider'
import React from 'react'
import { createRoot, hydrateRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import '@/lib/i18n' // needs to be bundled
import '@/assets/styles/main.css' // All of our styles
import { isTouchDevice } from '@/utils/device'
import AppRouter from './router'

const { PUBLIC_URL } = process.env

window.isTouchDevice = isTouchDevice()

// See https://reactjs.org/docs/strict-mode.html
const StrictApp = () => (
<Provider>
<BrowserRouter basename={PUBLIC_URL}>
<AppRouter />
</BrowserRouter>
</Provider>
)

const rootElement = document.getElementById('root')

// hydrate is required by react-snap.
if (rootElement.hasChildNodes()) {
hydrateRoot(rootElement, <StrictApp />)
} else {
const root = createRoot(rootElement)
root.render(<StrictApp />)
}
31 changes: 31 additions & 0 deletions src/app/router.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { Suspense, lazy } from 'react'
import { Route, Routes } from 'react-router-dom'
import App from './App'

// Every route - we lazy load so that each page can be chunked
// NOTE that some of these chunks are very small. We should optimize
// which pages are lazy loaded in the future.
const Map = lazy(() => import('@/pages/Map'))
const Simulation = lazy(() => import('@/pages/Simulation'))
const NotFound = lazy(() => import('@/pages/NotFound'))
const Impressum = lazy(() => import('@/pages/Impressum'))
const Datenschutz = lazy(() => import('@/pages/Datenschutz'))
const About = lazy(() => import('@/pages/About'))

const AppRouter = () => {
return (
<Suspense fallback={<App />}>
<Routes>
<Route path='/' element={<Map />} />
<Route path='/simulation/:lon/:lat' element={<Simulation />} />
<Route path='/anleitung' element={<About />} />
<Route path='/about' element={<About />} />
<Route path='/impressum' element={<Impressum />} />
<Route path='/datenschutz' element={<Datenschutz />} />
<Route path='*' element={<NotFound />} />
</Routes>
</Suspense>
)
}

export default AppRouter
File renamed without changes.
45 changes: 45 additions & 0 deletions src/components/layout/AppLayout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Box } from '@chakra-ui/react'
import PropTypes from 'prop-types'
import React from 'react'

const AppLayout = ({ children }) => {
return (
<Box
as='div'
display='flex'
margin='0'
maxWidth='100%'
opacity={1}
padding={0}
width='100vw'
height='100vh'
position='fixed'
left={0}
top={0}
flexDirection='column'
>
<Box
display='flex'
flexDirection='column'
justifyContent='space-between'
minWidth={0}
minHeight={0}
overflow='hidden'
flexGrow={1}
width='100%'
height='100%'
>
{children}
</Box>
</Box>
)
}

AppLayout.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
}

export default AppLayout
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import i18n from 'i18next'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { attributions, licenseLinks } from '../data/dataLicense'
import { attributions, licenseLinks } from '@/constants/licenses'

const WrapperForLaptopDevice = ({ children }) => {
return (
Expand Down
File renamed without changes.
2 changes: 2 additions & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './colors'
export * from './licenses'
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button, Input, List } from '@chakra-ui/react'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { processAddress } from '../../simulation/location'
import { processAddress } from '@/features/simulation/core/location'

export default function SearchField({ callback }) {
const [inputValue, setInputValue] = useState('')
Expand Down
4 changes: 4 additions & 0 deletions src/features/map/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Map Feature - Public API

export { default as MapPopup } from './components/MapPopup'
export { default as SearchField } from './components/SearchField'
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import * as THREE from 'three'
import { Matrix4 } from 'three'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { attributions } from '../data/dataLicense'
import { coordinatesLonLat, projectToWebMercator } from './location'
import { attributions } from '@/constants/licenses'
import {
coordinatesLonLat,
projectToWebMercator,
} from '@/features/simulation/core/location'

let federalState = null

Expand Down
File renamed without changes.
File renamed without changes.
12 changes: 6 additions & 6 deletions src/simulation/main.js → src/features/simulation/core/main.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ShadingScene, colormaps } from '@openpv/simshady'
import * as THREE from 'three'
import { c0, c1, c2 } from '../data/constants'
import { c0, c1, c2 } from '@/constants/colors'
import {
createSkydomeURL,
downloadBuildings,
getFederalState,
} from './download'
import { VEGETATION_DEM } from './elevation'
import { coordinatesWebMercator } from './location'
import { processGeometries } from './preprocessing'
import { processVegetationData } from './processVegetationTiffs'
} from '@/features/simulation/core/download'
import { VEGETATION_DEM } from '@/features/simulation/core/elevation'
import { coordinatesWebMercator } from '@/features/simulation/core/location'
import { processGeometries } from '@/features/simulation/core/preprocessing'
import { processVegetationData } from '@/features/simulation/core/processVegetationTiffs'

export async function mainSimulation(location) {
// Clear previous attributions if any
Expand Down
13 changes: 13 additions & 0 deletions src/features/simulation/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Simulation Feature - Public API

// Core simulation functions
export { mainSimulation } from './core/main'
export { elevation } from './core/elevation'
export { download } from './core/download'
export { location } from './core/location'
export { preprocessing } from './core/preprocessing'
export { processVegetationTiffs } from './core/processVegetationTiffs'

// UI Components
export { default as SavingsCalculation } from './components/SavingsCalculation'
export * from './components/savingsCalculator'
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { SceneContext } from '@/components/context'
import { SceneContext } from '@/features/three-viewer/context/SceneContext'

/**
* Displays mouse hover information for slope, azimuth, and yield.
Expand Down
Loading