Finish responsive UI

This commit is contained in:
Fuhrmann 2023-10-19 11:07:33 +02:00
parent 827273d84f
commit 2c0381a362
8477 changed files with 59705 additions and 176 deletions

149
app/about/page.tsx Normal file
View File

@ -0,0 +1,149 @@
import Link from 'next/link';
export default function About() {
return (
<div className="absolute top-16 bottom-0 left-0 right-0 w-full px-[33%] flex flex-col items-center pt-8 pb-8 overflow-y-auto">
<h1 className="text-xl mb-4">Informationen über diese Applikation</h1>
<p>
Diese Applikation unterstützt die Planung oberflächennaher geothermischer Anlagen. Es können geothermisch
relevante Parameter, und mögliche rechtliche Einschränkungen und Hinweise abgefragt werden. Außerdem können
standortspezifisch Berechnungen in Bezug auf die gewünschte Dimensionierung der geothermischen Anlage
durchgeführt werden. Die der Applikation zugrunde liegenden Daten wurden im Rahmen des Green Energy Lab -
Spatial Energy Planning Projekts erstellt. Nähere Informationen über dieses Projekt finden Sie unter{' '}
<Link href="https://www.waermeplanung.at">https://www.waermeplanung.at</Link>. Die Untersuchungsgebiete des
Projekts umfassen Wien, den Dauersiedlungsraum Salzburg und ausgewählte Gebiete in der Steiermark. Diese
Applikation beschränkt sich ausschließlich auf Wien.
</p>
<h1 className="text-xl mb-4 mt-6">Zusatzinfo über interaktive Sondenfeldberechnung </h1>
<p>
Das Programm berechnet die mögliche Leistung und Energie, die aus dem vorgegebenen Sondenfeld gewonnen werden
kann. Dabei wird die Geometrie des Sondenfeldes (Lage, Tiefe, Sondenabstand) vom Benutzer interaktiv vorgegeben.
Für dieses Sondenfeld wird zuerst die g-Funktion berechnet und danach das maximale Potenzial (Leistung und
Jahresenergie) ermittelt. Das ermittelte Potenzial berücksichtigt die Betriebsweise (siehe unten) und ist auf
die Grenzwerte der mittleren Fluidtemperturen ausgelegt. Im Heizbetrieb werden -1.5 °C nicht unterschritten und
im Kühlbetrieb werden 28 °C nicht überschritten. Die Betriebsweise wird vereinfacht in vier Phasen pro Jahr
unterteilt: Heizbetrieb Stillstand Kühlbetrieb - Stillstand.
</p>
<h2 className="text-lg mb-2 mt-4">Betriebsweise</h2>
<p>
Die Betriebsfunktion kann durch Angabe der gebäudeseitigen Heiz- und Kühlleistung sowie der
Jahresbetriebsstunden für Heizen und Kühlen vorgegeben werden. In diesem Fall wird die erzielbare Sondenleistung
für das benutzerdefinierte Sondenfeld so ermittelt, dass das Leistungsverhältnis zwischen Heizen und Kühlen
eingehalten wird. Es kann auch ein reiner Heiz- bzw. Kühlbetrieb vorgegeben werden. Bei der benutzerdefinierten
Betriebsweise wird auch ein Deckungsgrad berechnet, der angibt wieviel Prozent des angegebenen Bedarfs durch das
vorgegebene Sondenfeld abgedeckt werden kann. Wird die Betriebsfunktion nicht durch den Benutzer vorgegeben, so
wird mit einem Normbetrieb gerechnet. Dabei werden die Jahresbetriebsstunden für Heizen und Kühlen aus der
standortbezogenen Bodentemperatur für ein typisches Wohnhaus herangezogen. Die mittlere Oberflächentemperatur
des Bodens wird dabei aus den Ressourcenkarten für den Standort abgefragt. Ein Deckungsgrad wird hier nicht
ausgegeben. Ist die Jahresenergiebilanz des Sondenfeldes nicht ausgeglichen (mit einer Toleranz von +/- 10 %)
wird zusätzlich die Betriebsweise saisonaler Speicherbetrieb gerechnet. Wenn der Wärmeentzug überwiegt, wird
automatisch eine Zusatzquelle verwendet, welche die Bilanz mit dem zusätzlichen Wärmeeintrag ausgleicht. Wenn
der Wärmeeintrag überwiegt, wird automatisch eine Zusatzsenke zur Betriebsfunktion hinzugefügt, welche die
Bilanz mit einem zusätzlichen Wärmeentzug ausgleicht. Dabei wird angegeben, um wieviel Prozent die
Leistungsfähigkeit des Sondenfeldes im Speicherbetrieb gesteigert werden kann. Ist der gegenseitige
Sondenabstand größer als 6 m wird ein Hinweis ausgegeben, dass der gegenseitige Sondenabstand auf 5 m reduziert
werden kann. Die mittlere Wärmeleitfähigkeit und die mittlere Untergrundtemperatur von 0 bis 100 m Tiefe werden
aus den Ressourcenkarten für das ausgewählte Grundstück übernommen und fließen in die Berechnung ein. Ist die
Vorgabe der Sondentiefe größer als 100 m, so wird die Untergrundtemperatur mit einem Gradienten von 0.03 °C pro
Meter mit der Tiefe erhöht. Zusätzlich wird eine Grafik mit der Entwicklung der mittleren Fluidtemperatur in
Zusammenhang mit der berechneten Betriebsfunktion ausgegeben.
</p>
<h2 className="text-lg mb-2 mt-4">Zusätzliche Parameter für die Berechnung</h2>
<p>
Die folgenden Parameter werden für alle Simulationen verwendet und können nicht durch eine Eingabe verändert
werden. <br></br>
<br></br>
Simulationsjahre: 20 Jahre<br></br>
Volumetrische Wärmekapazität des Erdreichs: 2.2 MJ/m³/K<br></br>
Sondenkopf Überdeckung: 1 m <br></br>
Bohrradius: 0.075 m <br></br>
Sondentyp: Duplex 32 mm, 0.04 m Rohrabstand<br></br>
Wärmeträgermedium: Ethanol 12 % <br></br>
Massenstrom pro Sonde: 0.4 kg/s<br></br>
Wärmeleitfähigkeit der Verpressung: 2 W/m/K
</p>
<h2 className="text-lg mb-2 mt-4">Grenztemperaturen</h2>
<p>
Minimale mittlere Fluidtemperatur am Ende der Heizsaison: -1.5 °C <br></br>
Maximale mittlere Fluidtemperatur am Ende der Kühlsaison: 28 °C <br></br>
<br></br>
Das Sondenfeld wird im Heizbetrieb auf die minimale Grenztemperatur ausgelegt, im Kühlbetrieb auf die maximale
Grenztemperatur.
</p>
<h2 className="text-lg mb-2 mt-4">Leistungszahlen</h2>
<p className="w-full">Folgende Leistungszahlen der Wärmepumpe im Heiz- und Kühlbetrieb werden berücksichtigt:</p>
<dl className="mt-4">
<dt>COP</dt>
<dd className="mb-2">
Leistungszahl der Wärmepumpe im Heizbetrieb (Coefficient of Performance): Die Leistungszahl für Heizen (COP)
wird soleseitig immer auf die untere Grenztemperatur der Erdwärmesonden (Vorlauf -3 / Rücklauf 0 °C)
ausgelegt. Wasserseitig wird zur Berechnung des COP die vorgegebene Heizungs-Vorlauftemperatur verwendet,
welche ohne Nutzereingabe auf 35 °C voreingestellt ist. Diese Leistungszahl gilt also für den Extremfall, wenn
die Fluidtemperatur der Sonden den unteren Grenzwert erreicht haben (in der Regel am Ende der Heizsaison nach
20 Betriebsjahren).
</dd>
<dt>JAZ</dt>
<dd className="mb-2">
Jahresarbeitszahl oder saisonale Leistungszahl der Wärmepumpe im Heizbetrieb: Die Berechnung der
Jahresarbeitszahl im Heizbetrieb (JAZ) berücksichtigt die Sole-Fluidtemperaturen jeder Betriebsstunde im Jahr,
berechnet den COP und bildet daraus den Mittelwert über alle Betriebsstunden.
</dd>
<dt>EER</dt>
<dd className="mb-2">
Leistungszahl der Wärmepumpe im Kühlbetrieb (Energy Efficiency Rating): Die Leistungszahl für Kühlen (EER) ist
statisch auf eine Fluidtemperatur von 18 °C auf der kalten Seite (gebäudeseitig) und 30 °C auf der warmen
Seite ausgelegt (erdseitig).
</dd>
<dt>SEER</dt>
<dd className="mb-2">
Saisonale Leistungszahl der Wärmepumpe im Kühlbetrieb (Seasonal Energy Efficiency Rating): Die Berechnung der
saisonalen Leistungszahl im Kühlbetrieb (SEER) berücksichtigt die berechneten Fluid-Temperaturen aller
Kühlbetriebsstunden, insbesondere werden Fluidtemperaturen unterhalb 18 °C für die freie Kühlung ohne
Wärmepumpe berücksichtigt. Ohne Wärmepumpe ist die Kühlung besonders effizient, daher wird für die
Betriebsstunden mit freier Kühlung pauschal ein EER-Wert von 20 angenommen. Die Berechnungsformel basiert auf
Messwerte verschiedener Betriebspunkte einer realen Sole-Wasser Wärmepumpe in der Klimakammer und wurde im FFG
Projekt ZWEIFELDSPEICHER ermittelt.
</dd>
</dl>
<h1 className="text-xl mb-4 mt-6">Verwendete 3rd-Party Software</h1>
<p>
Die Berechnungen für Erdwärmesonden werden mit dem Python-Modul <code>pygfunction</code> durchgeführt (siehe{' '}
<Link href="https://pypi.org/project/pygfunction/">https://pypi.org/project/pygfunction/</Link>
).
</p>
<blockquote className="italic font-semibold text-left mt-4">
<p>
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS &quot;AS IS&quot; AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
</p>
<p>Copyright (c) 2017-2022, Massimo Cimmino All rights reserved.</p>
</blockquote>
<h1 className="text-xl mb-4 mt-6">Hinweise und Haftungsausschluss</h1>
<p>
Die thematischen Inhalte auf unserem Webportal dienen dazu, einen Überblick über Potentiale und Konflikte in
Zusammenhang mit geothermischen Anlagen zu geben. Sie ersetzen keine detaillierten Planungen. Aus unseren Karten
ergibt sich keinerlei Genehmigungsanspruch einer geplanten Nutzung gegenüber den zuständigen Behörden. Der
Anbieter dieses Webportals und der damit verbundenen Dienstleistungen übernimmt keine Haftung für Schäden, die
durch den ungeeigneten Gebrauch des Webportals entstehen.
</p>
<h1 className="text-xl mb-4 mt-6">Kontakt</h1>
<ul>
<li>GeoSphere Austria</li>
<li>Hohe Warte 38, 1190 Wien</li>
<li>
<Link href="mailto:geothermie@geosphere.at">geothermie@geosphere.at</Link>
</li>
<li>
<Link href="https://geosphere.at">https://www.geosphere.at</Link>
</li>
</ul>
</div>
);
}

File diff suppressed because it is too large Load Diff

22
app/api/pythonShell.js Normal file
View File

@ -0,0 +1,22 @@
import { PythonShell } from 'python-shell';
const scriptFile = './app/api/BHEseppy_query_V50_MWP_beta6.py';
export async function runPythonShell(options) {
try {
const results = await PythonShell.run(scriptFile, options);
let resultList = [];
if (results && results.length > 0) {
let resultString = results[results.length - 1];
resultList = resultString
.replace(/[\[\]]/g, '')
.split(',')
.map((entry) => entry.trim().replaceAll("'", ''));
}
return resultList;
} catch (e) {
console.error(e);
}
}

28
app/api/route.ts Normal file
View File

@ -0,0 +1,28 @@
import { NextResponse } from 'next/server';
import { runPythonShell } from './pythonShell';
export async function POST(request: Request) {
const req = await request.json();
let options = {
args: [
req.BT,
req.GT,
req.WLF,
req.BS_HZ_Norm,
req.BS_KL_Norm,
req.BS_HZ,
req.BS_KL,
req.P_HZ,
req.P_KL,
req.boreDepth,
req.heating,
req.points,
req.heating,
],
};
if (options.args.every((arg) => typeof arg !== 'undefined')) {
const results = await runPythonShell(options);
return NextResponse.json(results);
}
}

View File

@ -0,0 +1,43 @@
import { useState, useRef, forwardRef, PropsWithChildren, ForwardedRef, useImperativeHandle } from 'react';
type Properties = {
title: string;
open: boolean;
};
export default forwardRef(function Collapsible(
props: PropsWithChildren<Properties>,
outerRef: ForwardedRef<HTMLDivElement> | undefined
) {
const [opened, setOpened] = useState<boolean>(props.open);
const [previouslyOpened, setPreviouslyOpened] = useState<boolean>(props.open);
const innerRef = useRef<HTMLDivElement | null>(null);
useImperativeHandle(outerRef, () => innerRef.current!, []);
if (props.open !== previouslyOpened) {
setOpened(props.open);
setPreviouslyOpened(props.open);
}
const handleClick = () => {
setOpened(!opened);
if (innerRef && innerRef.current) {
innerRef.current.classList.toggle('hidden');
}
};
return (
<div>
<div
onClick={handleClick}
className="bg-gray-700 text-white cursor-pointer p-4 w-full border-0 hover:bg-gray-500 h-12 items-center justify-between mt-px text-sm xl:text-base leading:normal"
>
{props.title} <span className="float-right">{opened ? '-' : '+'}</span>
</div>
<div className={`px-2 pt-2 overflow-y-auto bg-white text-sm ${opened ? '' : 'hidden'}`} ref={innerRef}>
{props.children}
</div>
</div>
);
});

61
app/components/footer.tsx Normal file
View File

@ -0,0 +1,61 @@
import Collapsible from './collapsible';
import Link from 'next/link';
const tableDataCSS = 'w-full break-words border-b border-solid border-gray-300';
export default function Footer() {
return (
<>
<Collapsible title="Haftungsausschluss" open={true}>
<table id="disclaimer" className="table-fixed w-full mb-4">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>
<tr>
<td className={tableDataCSS}>
Die thematischen Inhalte dieser Webapplikation dienen dazu, einen Überblick über Potentiale und
Konflikte in Zusammenhang mit geothermischen Anlagen zu geben. Sie ersetzen keine detaillierten
Planungen. Aus unseren Karten ergibt sich keinerlei Genehmigungsanspruch einer geplanten Nutzung
gegenüber den zuständigen Behörden. Der Anbieter dieser Webapplikation und der damit verbundenen
Dienstleistungen übernimmt keine Haftung für Schäden, die durch den ungeeigneten Gebrauch des Webportals
entstehen.
</td>
</tr>
</tbody>
</table>
</Collapsible>
<Collapsible title="Kontakt" open={true}>
<table id="contact" className="table-fixed w-full">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>
<tr>
<td>
<Link
href="mailto:geothermie@geosphere.at"
className="underline underline-offset-4 decoration-transparent transition duration-300 ease-in-out hover:decoration-red-700"
>
geothermie@geosphere.at
</Link>
</td>
</tr>
<tr>
<td>Department für Rohstoffgeologie und Geoenergie</td>
</tr>
<tr>
<td>Hohe Warte 38</td>
</tr>
<tr>
<td className={tableDataCSS}>1190 Wien</td>
</tr>
</tbody>
</table>
</Collapsible>
</>
);
}

View File

@ -0,0 +1,5 @@
import type { ReactNode } from 'react';
export default function Success(props: { children: ReactNode }) {
return <p className="w-full whitespace-normal break-normal bg-green-300 text-green-700 mb-1 p-4">{props.children}</p>;
}

View File

@ -0,0 +1,7 @@
import type { ReactNode } from 'react';
export default function Warning(props: { children: ReactNode }) {
return (
<p className="w-full whitespace-normal break-normal bg-orange-300 text-orange-700 mb-1 p-4">{props.children}</p>
);
}

38
app/config/config.js Normal file
View File

@ -0,0 +1,38 @@
// spatial reference system
export const SRS = 31256;
// layer URLs
export const AMPEL_EWS_URL = 'https://gis.geosphere.at/maps/rest/services/geothermie/ampelkarte_ews/MapServer';
export const AMPEL_GWWP_URL = 'https://gis.geosphere.at/maps/rest/services/geothermie/ampelkarte_gwwp/MapServer';
export const RESOURCES_EWS_URL = 'https://gis.geosphere.at/maps/rest/services/geothermie/potentialkarte_ews/MapServer';
export const RESOURCES_GWWP_URL =
'https://gis.geosphere.at/maps/rest/services/geothermie/potentialkarte_gwwp/MapServer';
export const BEV_KATASTER_URL = 'https://data.bev.gv.at/geoserver/BEVdataKAT/wms';
export const BASEMAP_AT_URL = 'https://mapsneu.wien.gv.at/basemap31256neu';
// text templates for info panel
export const textTemplates = {
0: ['Die mittlere jährliche Bodentemperatur beträgt laut Satellitendaten (MODIS) rund ', ' °C.'],
1: ['Die mittlere Temperatur des Untergrunds für eine Tiefe von 0 bis 100 m beträgt rund ', ' °C.'],
2: [
'Die mittlere konduktive Wärmeleitfähigkeit des Untergrunds für eine Tiefe von 0 bis 100 m beträgt rund ',
' W/m/K.',
],
3: [
'Die Entzugsleistung einer 100 m tiefen Einzelsonde im standortbezogenen Normbetrieb (Heizen und Kühlen mit Normbetriebsstunden eines typischen Wohngebäudes am Standort) beträgt am Grundstück rund ',
' W/lfm.',
],
4: [
'Die Entzugsleistung einer 100 m tiefen Einzelsonde im saisonalem Speicherbetrieb (die im Winter zur Heizung entzogene Wärme wird im Sommer vollständig wieder zurückgegeben) beträgt am Grundstück rund ',
' W/lfm.',
],
5: [
`Die flächenspezifische Jahresenergie eines 1156 m² großen und 100 m tiefen Sondenfeldes im standortbezogenen Normbetrieb (4 x 4 Sonden mit je 10 m Abstand - Heizen und Kühlen mit Normbetriebsstunden eines typischen Wohngebäudes am Standort) beträgt rund `,
' kWh/m²/a.',
],
6: [
`Die flächenspezifische Jahresenergie eines 1156 m² großen und 100 m tiefen Sondenfeldes im saisonalem Speicherbetrieb (7 x 7 Sonden mit je 5 m Abstand - die im Winter zur Heizung entzogene Wärme wird im Sommer vollständig wieder zurückgegeben) beträgt rund `,
' kWh/m²/a.',
],
};

13
app/daten/page.tsx Normal file
View File

@ -0,0 +1,13 @@
import Link from 'next/link';
export default function Daten() {
return (
<main className="w-full">
<div className="flex flex-col items-center pt-32">
<p>
Hier kommen die Links zu <Link href="https://www.tethys.at/">Tethys</Link>
</p>
</div>
</main>
);
}

View File

@ -0,0 +1,25 @@
.esri-component {
max-height: 80%;
}
.esri-ui-corner .esri-component.esri-layer-list.esri-widget.esri-widget--panel {
}
.esri-component.esri-legend.esri-widget.esri-widget--panel {
}
.esri-ui-top-left.esri-ui-corner {
height: 95%;
}
.esri-layer-list {
background-color: #fff;
}
.esri-search {
width: 300px;
}
.esri-legend__layer-child-table > .esri-legend__layer-caption {
display: none;
}

View File

@ -0,0 +1,258 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { useMediaQuery } from 'react-responsive';
import ArcGISMap from '@arcgis/core/Map';
import Extent from '@arcgis/core/geometry/Extent';
import MapView from '@arcgis/core/views/MapView';
import Basemap from '@arcgis/core/Basemap';
import MapImageLayer from '@arcgis/core/layers/MapImageLayer';
import WMTSLayer from '@arcgis/core/layers/WMTSLayer';
import SpatialReference from '@arcgis/core/geometry/SpatialReference';
import Search from '@arcgis/core/widgets/Search';
import ScaleBar from '@arcgis/core/widgets/ScaleBar';
import GraphicsLayer from '@arcgis/core/layers/GraphicsLayer';
import Graphic from '@arcgis/core/Graphic';
import LayerList from '@arcgis/core/widgets/LayerList';
import Zoom from '@arcgis/core/widgets/Zoom';
import Legend from '@arcgis/core/widgets/Legend';
import FeatureLayer from '@arcgis/core/layers/FeatureLayer';
import Polygon from '@arcgis/core/geometry/Polygon';
import esriConfig from '@arcgis/core/config';
import { watch } from '@arcgis/core/core/reactiveUtils';
import type SimpleFillSymbol from '@arcgis/core/symbols/SimpleFillSymbol';
import './esri-ui-grundlagenkarte.css';
import { Vienna, Austria } from '@/public/borders-vienna-austria';
import { BASEMAP_AT_URL, AMPEL_EWS_URL, RESOURCES_EWS_URL, SRS } from '@/app/config/config';
import getAddress from '@/app/utils/getAddress';
import identifyAllLayers from '@/app/utils/identify';
import takeScreenshot from '@/app/utils/screenshot';
import { updateAmpelkarteEWS } from '@/redux/ampelkarteEWSSlice';
import { updateResourcesEWS } from '@/redux/resourcesEWSSlice';
import { updateScreenshot } from '@/redux/screenshotSlice';
import { useAppDispatch } from '@/redux/hooks';
import PanelGrundlagenkarte from './panel-grundlagenkarte';
// set path for local assets
esriConfig.assetsPath = '/assets';
export default function MapComponent() {
const dispatch = useAppDispatch();
const mapDiv = useRef<HTMLDivElement | null>(null);
const [address, setAddress] = useState<string[] | null>([]);
const isMobile = useMediaQuery({ maxWidth: 480 });
useEffect(() => {
let handle: IHandle;
let view: MapView;
if (mapDiv.current) {
dispatch(updateAmpelkarteEWS([]));
dispatch(updateResourcesEWS([]));
dispatch(updateScreenshot(''));
view = new MapView({
container: mapDiv.current,
extent: new Extent({
xmin: -19000,
ymin: 325000,
xmax: 29000,
ymax: 360000,
spatialReference: new SpatialReference({ wkid: SRS }),
}),
popupEnabled: false,
});
let viennaGraphic = new Graphic({
geometry: new Polygon({ rings: [Austria, Vienna], spatialReference: { wkid: SRS } }),
symbol: {
type: 'simple-fill',
color: [209, 213, 219, 0.65],
style: 'solid',
outline: {
color: 'white',
width: 0,
},
} as unknown as SimpleFillSymbol,
});
// graphic layers
let viennaGraphicsLayer = new GraphicsLayer({ title: 'Wien', listMode: 'hide' });
viennaGraphicsLayer.add(viennaGraphic);
const ampelkarte_ews = new FeatureLayer({
url: AMPEL_EWS_URL + '/0',
title: 'Mögliche Einschränkungen',
visible: false,
listMode: 'show',
opacity: 0.5,
});
const resources_ews = new MapImageLayer({
title: 'Ressourcen',
url: RESOURCES_EWS_URL,
visible: false,
listMode: 'show',
opacity: 0.5,
});
// basemap in Viennese coordinate system due to tranformation inaccuracies from MGI to WGS84
// default transformation in ArcGIS API from MGI to WGS84 is 1306
// transformation 1618 is recommended
const basemap_at = new WMTSLayer({
url: BASEMAP_AT_URL,
listMode: 'hide',
});
let basemap = new Basemap({
baseLayers: [basemap_at],
title: 'basemap.at',
id: 'basemap.at',
spatialReference: { wkid: SRS },
});
let arcgisMap = new ArcGISMap({
basemap: basemap,
layers: [resources_ews, ampelkarte_ews, viennaGraphicsLayer],
});
const layerList = new LayerList({
view,
listItemCreatedFunction: addPanelInfo,
});
const search = new Search({
view,
popupEnabled: true,
});
const scaleBar = new ScaleBar({
view: view,
unit: 'metric',
});
const zoom = new Zoom({
view,
layout: 'horizontal',
});
const legend = new Legend({
view,
});
// register event handler for mouse clicks
view.on('immediate-click', (event) => {
if (setAddress && dispatch) {
takeScreenshot(view, event.mapPoint, dispatch, true);
getAddress(event.mapPoint, setAddress);
identifyAllLayers(view, event.mapPoint, dispatch, 'EWS');
}
});
// add map to view
view.map = arcgisMap;
// add UI components
view.ui.components = [];
if (!isMobile) {
view.ui.add([zoom, search, layerList], 'top-left');
view.ui.add(scaleBar, 'bottom-left');
}
view.when(() => {
handle = watch(
() => view.map?.layers?.map((layer) => layer.visible),
() => {
if (view.map?.layers?.some((layer) => layer.title !== 'Wien' && layer.visible === true)) {
view.ui?.add(legend, 'top-left');
} else {
view.ui?.remove(legend);
}
}
);
});
}
return () => {
handle?.remove();
view?.destroy();
};
}, [dispatch, isMobile, mapDiv]);
return (
<div ref={mapDiv} className="absolute top-16 bottom-0 w-full">
<PanelGrundlagenkarte address={address}></PanelGrundlagenkarte>
</div>
);
}
const addPanelInfo = (event: any) => {
let item = event.item;
switch (item.layer.id) {
case 0:
item.panel = {
content: 'mittlere jährliche Bodentemperatur laut Satellitendaten (MODIS)',
className: 'esri-icon-description',
};
break;
case 1:
item.panel = {
content: 'mittlere Temperatur des Untergrunds für eine Tiefe von 0 bis 100 m',
className: 'esri-icon-description',
};
break;
case 2:
item.panel = {
content: 'mittlere konduktive Wärmeleitfähigkeit des Untergrunds für eine Tiefe von 0 bis 100 m',
className: 'esri-icon-description',
};
break;
case 3:
item.panel = {
content:
'Entzugsleistung einer 100 m tiefen Einzelsonde im standortbezogenen Normbetrieb (Heizen und Kühlen mit Normbetriebsstunden eines typischen Wohngebäudes am Standort)',
className: 'esri-icon-description',
};
break;
case 4:
item.panel = {
content:
'Entzugsleistung einer 100 m tiefen Einzelsonde im saisonalem Speicherbetrieb (die im Winter zur Heizung entzogene Wärme wird im Sommer vollständig wieder zurückgegeben)',
className: 'esri-icon-description',
};
break;
case 5:
item.panel = {
content:
'flächenspezifische Jahresenergie eines 1156 m² großen und 100 m tiefen Sondenfeldes im standortbezogenen Normbetrieb (4 x 4 Sonden mit je 10 m Abstand - Heizen und Kühlen mit Normbetriebsstunden eines typischen Wohngebäudes am Standort)',
className: 'esri-icon-description',
};
break;
case 6:
item.panel = {
content:
'flächenspezifische Jahresenergie eines 1156 m² großen und 100 m tiefen Sondenfeldes im saisonalem Speicherbetrieb (7 x 7 Sonden mit je 5 m Abstand - die im Winter zur Heizung entzogene Wärme wird im Sommer vollständig wieder zurückgegeben)',
className: 'esri-icon-description',
};
break;
case 7:
item.panel = {
content: 'mittlere Jahresbetriebsstunden für Heizen',
className: 'esri-icon-description',
};
break;
case 8:
item.panel = {
content: 'mittlere Jahresbetriebsstunden für Kühlen',
className: 'esri-icon-description',
};
break;
default:
break;
}
};

View File

@ -0,0 +1,9 @@
'use client';
import dynamic from 'next/dynamic';
const MapComponent = dynamic(() => import('./grundlagenkarte'), { ssr: false });
export default function Grundlagenkarte() {
return <MapComponent></MapComponent>;
}

View File

@ -0,0 +1,170 @@
'use client';
import Image from 'next/image';
import { useRef, useState } from 'react';
import { useMediaQuery } from 'react-responsive';
import { useAppSelector } from '@/redux/hooks';
import { TableAmpelkarteEWS } from '@/app/ews/table-ampelkarte-ews';
import Collapsible from '@/app/components/collapsible';
import Footer from '@/app/components/footer';
import Warning from '@/app/components/warning';
interface Resource {
layerId: number;
layerName: string;
feature: any;
}
export const textTemplates: { [key: number]: string[] } = {
0: ['Die mittlere jährliche Bodentemperatur beträgt laut Satellitendaten (MODIS) rund ', ' °C.'],
1: ['Die mittlere Temperatur des Untergrunds für eine Tiefe von 0 bis 100 m beträgt rund ', ' °C.'],
2: [
'Die mittlere konduktive Wärmeleitfähigkeit des Untergrunds für eine Tiefe von 0 bis 100 m beträgt rund ',
' W/m/K.',
],
3: [
'Die Entzugsleistung einer 100 m tiefen Einzelsonde im standortbezogenen Normbetrieb (Heizen und Kühlen mit Normbetriebsstunden eines typischen Wohngebäudes am Standort) beträgt am Grundstück rund ',
' W/lfm.',
],
4: [
'Die Entzugsleistung einer 100 m tiefen Einzelsonde im saisonalem Speicherbetrieb (die im Winter zur Heizung entzogene Wärme wird im Sommer vollständig wieder zurückgegeben) beträgt am Grundstück rund ',
' W/lfm.',
],
5: [
`Die flächenspezifische Jahresenergie eines 1156 m² großen und 100 m tiefen Sondenfeldes im standortbezogenen Normbetrieb (4 x 4 Sonden mit je 10 m Abstand - Heizen und Kühlen mit Normbetriebsstunden eines typischen Wohngebäudes am Standort) beträgt rund `,
' kWh/m²/a.',
],
6: [
`Die flächenspezifische Jahresenergie eines 1156 m² großen und 100 m tiefen Sondenfeldes im saisonalem Speicherbetrieb (7 x 7 Sonden mit je 5 m Abstand - die im Winter zur Heizung entzogene Wärme wird im Sommer vollständig wieder zurückgegeben) beträgt rund `,
' kWh/m²/a.',
],
};
const tableDataCSS = 'w-full break-words border-b border-solid border-gray-300';
export default function Panel({ address }: { address: string[] | null }) {
const isMobile = useMediaQuery({ maxWidth: 640 });
const [opened, setOpened] = useState<boolean>(true);
const innerRef = useRef<HTMLDivElement | null>(null);
const ampelkarte = useAppSelector((store) => store.ampelkarteEWS.value);
const resources: Resource[] = useAppSelector((store) => store.resourcesEWS.value);
const screenshot = useAppSelector((store) => store.screenshot.value);
// format values
const formatEWS = (layerId: number, layerName: string, value: string) => {
if (value !== 'NoData') {
if ([0, 1, 2, 4, 5, 6].includes(layerId)) {
value = parseFloat(value).toFixed(1);
} else {
value = parseFloat(value).toFixed(0);
}
return textTemplates[layerId][0] + value + textTemplates[layerId][1];
} else {
return layerName + ': keine Daten';
}
};
const handleClick = () => {
setOpened(!opened);
if (innerRef && innerRef.current) {
innerRef.current.classList.toggle('hidden');
}
};
return (
<div
className={`absolute top-4 right-4 ${opened && isMobile ? 'bottom-4' : ''} ${
opened && resources && resources.length > 0 ? 'bottom-4' : ''
} w-full md:w-1/3 xl:w-1/4 pl-8 md:pl-0`}
>
<div
className="bg-gray-700 text-white cursor-pointer p-4 w-full border-0 hover:bg-gray-500 h-12 items-center justify-between text-sm xl:text-base"
onClick={handleClick}
>
Abfrageergebnis <span className="float-right">{opened ? '-' : '+'}</span>
</div>
<div
className={`max-h-[calc(100%-3rem)] px-2 xl:px-4 py-4 overflow-y-auto bg-white text-sm ${
opened ? '' : 'hidden'
}`}
ref={innerRef}
>
{screenshot ? <Image src={screenshot} alt="Screenshot" width={1000} height={500}></Image> : null}
{address && address.length > 0 ? (
<>
<table id="address-table" className="table-fixed w-full mt-3 mr-0 mb-3 ml-0">
<tbody>
<tr>
<td>
{address[0]}
<br></br>
{address[1]} {address[3]}
</td>
</tr>
</tbody>
</table>
</>
) : (
<div className="pb-2">
<Warning>Mit einem Klick in die Karte können Sie die geothermischen Daten abfragen.</Warning>
</div>
)}
{resources && resources.length > 0 ? (
<>
<div className="pt-px"></div>
<Collapsible title="Ressourcen" open={true}>
<table id="resources-table" className="table-fixed w-full px-2 mb-4">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>
<tr className="w-full">
<th className="pt-4 pb-2 text-center">Ressourcen für vordefinierte Erdwärmesondenanlage</th>
</tr>
{resources.slice(3, 7).map((result) => {
return (
<tr className="w-full" key={result.layerId}>
<td className={tableDataCSS}>
{formatEWS(
result.layerId,
result.layerName,
result.feature.attributes['Classify.Pixel Value']
)}
</td>
</tr>
);
})}
<tr className="w-full">
<th className="pt-4 pb-2 text-center">Standortabhängige Parameter</th>
</tr>
{resources.slice(0, 3).map((result) => {
return (
<tr className="w-full" key={result.layerId}>
<td className={tableDataCSS}>
{formatEWS(
result.layerId,
result.layerName,
result.feature.attributes['Classify.Pixel Value']
)}
</td>
</tr>
);
})}
</tbody>
</table>
</Collapsible>
</>
) : null}
{ampelkarte && ampelkarte.length > 0 ? <TableAmpelkarteEWS results={ampelkarte}></TableAmpelkarteEWS> : null}
<Footer></Footer>
</div>
</div>
);
}

23
app/ews/page.tsx Normal file
View File

@ -0,0 +1,23 @@
import Link from 'next/link';
export default function Home() {
const linkCSS = 'hover:underline hover:decoration-red-700 hover:underline-offset-4';
return (
<main className="w-full">
<div className="flex flex-col items-center pt-32">
<ul className="mx-auto text-2xl font-semibold w-fit">
<li>
<Link href="/ews/grundlagenkarte" className={linkCSS}>
Zur Grundlagenkarte
</Link>
</li>
<li>
<Link href="/ews/potenzialberechnung" className={linkCSS}>
Zur Potenzialberechnung
</Link>
</li>
</ul>
</div>
</main>
);
}

View File

@ -0,0 +1,480 @@
'use client';
import {
useState,
useEffect,
PropsWithChildren,
ReactNode,
SetStateAction,
Dispatch,
ChangeEventHandler,
ForwardedRef,
forwardRef,
useRef,
} from 'react';
import { useMediaQuery } from 'react-responsive';
import { updateComputationResultEWS } from '@/redux/computationsEWSSlice';
import { useAppSelector, useAppDispatch } from '@/redux/hooks';
import type Polygon from '@arcgis/core/geometry/Polygon';
import Warning from '@/app/components/warning';
import { initializeCalculationsMenuHandlers } from './potenzialkarte';
import { calculateGrid } from './gridcomputer';
const InputSection = (props: { children: ReactNode }) => {
return <div className="pb-2">{props.children}</div>;
};
type Properties = {
isLoading: Dispatch<SetStateAction<boolean>>;
};
type Resource = { layerId: number; layerName: string; feature: any };
const css =
'peer block min-h-[auto] w-full border px-3 py-1 leading-normal outline-none transition-all duration-200 ease-linear motion-reduce:transition-none';
export default forwardRef(function CalculationsMenu(
{ isLoading }: PropsWithChildren<Properties>,
outerRef: ForwardedRef<HTMLDivElement> | undefined
) {
const isTablet = useMediaQuery({ maxWidth: 1024 });
const [polygon, setPolygon] = useState<Polygon | null>(null);
const [gridSpacing, setGridSpacing] = useState<number>(10);
const [boreDepth, setBoreDepth] = useState<number>(100);
const [BS_HZ, setBS_HZ] = useState<number>(-1);
const [BS_KL, setBS_KL] = useState<number>(-1);
const [P_KL, setP_KL] = useState<number>(-1);
const [P_HZ, setP_HZ] = useState<number>(-1);
const [points, setPoints] = useState<number[][]>([]);
const [heating, setHeating] = useState<number>(35);
const [opened, setOpened] = useState<boolean>(true);
const innerRef = useRef<HTMLDivElement | null>(null);
const cadastralData = useAppSelector((store) => store.cadastre.value);
const resources: Resource[] = useAppSelector((store) => store.resourcesEWS.value);
const dispatch = useAppDispatch();
// run python script with values from layers
const handlePythonCalculation = () => {
if (cadastralData && resources && points.length <= 300 && polygon) {
isLoading(true);
let pointsText = JSON.stringify(points);
const BT = parseFloat(
resources.find((result) => result.layerId === 0)?.feature?.attributes['Classify.Pixel Value']
);
const GT = parseFloat(
resources.find((result) => result.layerId === 1)?.feature?.attributes['Classify.Pixel Value']
);
const WLF = parseFloat(
resources.find((result) => result.layerId === 2)?.feature?.attributes['Classify.Pixel Value']
);
const BS_HZ_Norm = parseInt(
resources.find((result) => result.layerId === 7)?.feature?.attributes['Classify.Pixel Value']
);
const BS_KL_Norm = parseInt(
resources.find((result) => result.layerId === 8)?.feature?.attributes['Classify.Pixel Value']
);
let url = '/api';
const data = {
BT,
GT,
WLF,
BS_HZ_Norm,
BS_KL_Norm,
BS_HZ: BS_HZ === -1 ? 0 : BS_HZ,
BS_KL: BS_KL === -1 ? 0 : BS_KL,
P_HZ: P_HZ === -1 ? 0 : P_HZ,
P_KL: P_KL === -1 ? 0 : P_KL,
boreDepth,
points: pointsText,
heating,
};
if (
Object.values(data).every((x) => typeof x !== 'undefined' && x !== null) &&
!isNaN(BT) &&
!isNaN(GT) &&
!isNaN(WLF) &&
!isNaN(BS_HZ_Norm) &&
!isNaN(BS_KL_Norm)
) {
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then((res) => res.json())
.then((data) => {
// user defined input
const calculationMode = data[0];
const P_HZ_user = parseFloat(data[1]);
const P_KL_user = -parseFloat(data[2]);
const E_HZ_user = parseFloat(data[3]) / 1000;
const E_KL_user = -(parseFloat(data[4]) / 1000);
const cover = parseInt(data[5]);
const Pel_heatpump_user = parseFloat(data[6]);
const Pel_chiller_user = -parseFloat(data[7]);
const Eel_heatpump_user = parseFloat(data[8]) / 1000;
const Eel_chiller_user = -parseFloat(data[9]) / 1000;
const COP = parseFloat(data[15]);
const EER = parseFloat(data[16]);
const SCOP = parseFloat(data[17]);
const SEER = parseFloat(data[18]);
const Efactor_user = parseFloat(data[19]);
const imagehash = 'data:image/png;base64,' + data[20];
const imagehashSondenfeld = 'data:image/png;base64,' + data[21];
const GTcalc = parseFloat(data[22]);
const heizleistung = P_HZ_user + Pel_heatpump_user;
const heizarbeit = E_HZ_user + Eel_heatpump_user;
const kuehlleistung = P_KL_user - Pel_chiller_user;
const kuehlarbeit = E_KL_user - Eel_chiller_user;
// automatically balanced
const balanced = parseInt(data[23]);
const P_HZ_bal = parseFloat(data[24]);
const P_KL_bal = -parseFloat(data[25]);
const E_HZ_bal = parseFloat(data[26]) / 1000;
const E_KL_bal = -parseFloat(data[27]) / 1000;
const cover_bal = parseInt(data[28]);
const Pel_heatpump_bal = parseFloat(data[29]);
const Pel_chiller_bal = -parseFloat(data[30]);
const Eel_heatpump_bal = parseFloat(data[31]) / 1000;
const Eel_chiller_bal = -parseFloat(data[32]) / 1000;
const meanBoreholeSpacing = parseFloat(data[36]);
const cover_rise = parseFloat(data[37]);
const COP_bal = parseFloat(data[38]);
const EER_bal = parseFloat(data[39]);
const SCOP_bal = parseFloat(data[40]);
const SEER_bal = parseFloat(data[41]);
const Efactor_bal = parseFloat(data[42]);
const imagehashBal = 'data:image/png;base64,' + data[43];
const BS_HZ_bal = parseFloat(data[44]);
const BS_KL_bal = parseFloat(data[45]);
const T_radiator = parseInt(data[46]);
const heizleistungBal = P_HZ_bal + Pel_heatpump_bal;
const heizarbeitBal = Eel_heatpump_bal + E_HZ_bal;
const kuehlleistungBal = P_KL_bal - Pel_chiller_bal;
const kuehlarbeitBal = E_KL_bal - Eel_chiller_bal;
dispatch(
updateComputationResultEWS({
calculationMode,
points: points.length,
meanBoreholeSpacing,
boreDepth,
P_HZ,
P_KL,
BS_HZ,
BS_KL,
BS_HZ_Norm,
BS_KL_Norm,
P_HZ_user,
P_KL_user,
Pel_heatpump_user,
Pel_chiller_user,
E_HZ_user,
E_KL_user,
Efactor_user,
Eel_heatpump_user,
Eel_chiller_user,
cover,
imagehash,
imagehashSondenfeld,
balanced,
P_HZ_bal,
P_KL_bal,
Pel_heatpump_bal,
Pel_chiller_bal,
Eel_heatpump_bal,
Efactor_bal,
E_HZ_bal,
cover_bal,
imagehashBal,
heizleistung,
heizarbeit,
kuehlleistung,
kuehlarbeit,
heizleistungBal,
heizarbeitBal,
kuehlleistungBal,
GTcalc,
COP,
SCOP,
EER,
SEER,
BS_HZ_bal,
BS_KL_bal,
COP_bal,
EER_bal,
E_KL_bal,
SEER_bal,
SCOP_bal,
Eel_chiller_bal,
kuehlarbeitBal,
cover_rise,
T_radiator,
})
);
isLoading(false);
})
.catch((err) => {
dispatch(
updateComputationResultEWS({
error: JSON.stringify(err),
})
);
});
} else {
dispatch(
updateComputationResultEWS({
error: 'Aufgrund ungültiger Daten ist für dieses Grundstück keine Berechnung möglich.',
})
);
isLoading(false);
}
}
};
useEffect(() => {
// initialize callback functions
initializeCalculationsMenuHandlers(setPoints, setPolygon);
// cleanup
return () => {
// set polygon to null
setPolygon(null);
};
}, []);
// reset state
useEffect(() => {
setGridSpacing(10);
}, [polygon]);
const handleGridSpacing: ChangeEventHandler<HTMLInputElement> = (event) => {
let value = parseInt((event.target as HTMLInputElement).value);
if (value < 5) {
value = 5;
} else if (value > 15) {
value = 15;
}
setGridSpacing(value);
calculateGrid(polygon, value, setPoints);
};
const handleDepth: ChangeEventHandler<HTMLInputElement> = (event) => {
let value = parseInt((event.target as HTMLInputElement).value);
if (value > 250) {
value = 250;
} else if (value < 80) {
value = 80;
}
setBoreDepth(value);
};
const handleBS_HZ: ChangeEventHandler<HTMLInputElement> = (event) => {
let value = parseInt((event.target as HTMLInputElement).value);
if (value > 4379 || value < 0) {
value = -1;
}
if (value !== -1) {
setBS_HZ(value);
}
};
const handleBS_KL: ChangeEventHandler<HTMLInputElement> = (event) => {
let value = parseInt((event.target as HTMLInputElement).value);
if (event.target && event.target !== null) {
if (value > 4379 || value < 0) {
value = -1;
}
if (value !== -1) {
setBS_KL(value);
}
}
};
const handleP_HZ: ChangeEventHandler<HTMLInputElement> = (event) => {
let value = parseInt((event.target as HTMLInputElement).value);
if (value < 0) {
value = -1;
}
if (value !== -1) {
setP_HZ(value);
}
};
const handleP_KL: ChangeEventHandler<HTMLInputElement> = (event) => {
let value = parseInt((event.target as HTMLInputElement).value);
if (value < 0) {
value = -1;
}
if (value !== -1) {
setP_KL(value);
}
};
const handleKeyDown = (event: any) => {
event.preventDefault();
};
const handleHeating: ChangeEventHandler<HTMLSelectElement> = (event) => {
let value = parseInt((event.target as HTMLSelectElement).value);
setHeating(value);
};
const handleClick = () => {
setOpened(!opened);
if (innerRef && innerRef.current) {
innerRef.current.classList.toggle('hidden');
}
};
return (
<div
className={`absolute top-[70px] lg:top-24 left-4 ${
opened && isTablet ? 'bottom-12' : ''
} w-[calc(100%-2rem)] lg:w-[300px]`}
>
<div
onClick={handleClick}
className="bg-gray-700 text-white cursor-pointer p-4 w-full border-0 hover:bg-gray-500 h-12 items-center justify-between text-sm xl:text-base"
>
Berechnungsmenü <span className="float-right">{opened ? '-' : '+'}</span>
</div>
<div
className={`h-[calc(100%-3rem)] px-2 xl:px-4 pt-4 pb-2 overflow-y-auto bg-white text-sm ${
opened ? '' : 'hidden'
}`}
ref={innerRef}
>
<div className={!polygon ? 'bg-white opacity-50 z-50 pointer-events-none cursor-not-allowed pb-2' : 'pb-2'}>
<label>Sondenpunkte auswählen/zeichnen</label>
<div ref={outerRef}></div>
</div>
<InputSection>
<label htmlFor="gridspacing-input">Heizungsart </label>
<select id="gridspacing-input" onChange={handleHeating} className={css} disabled={polygon ? false : true}>
<option value={35}>Fußbodenheizung</option>
<option value={50}>Radiator</option>
</select>
</InputSection>
<InputSection>
<label htmlFor="gridspacing-input">Sondenabstand in Meter</label>
<input
type="number"
value={gridSpacing}
className={css}
onChange={handleGridSpacing}
onKeyDown={handleKeyDown}
placeholder="Wert zwischen 15 und 15 m (default=100)"
min="5"
max="15"
disabled={polygon ? false : true}
></input>
</InputSection>
<InputSection>
<label htmlFor="depth-input">Sondentiefe in Meter</label>
<input
className={css}
type="number"
min="80"
max="250"
placeholder="Wert zwischen 80 und 250 m (default=100)"
value={boreDepth}
onChange={handleDepth}
onKeyDown={handleKeyDown}
disabled={polygon ? false : true}
></input>
</InputSection>
<InputSection>
<label htmlFor="phz-input">Heizleistung in kW (optional)</label>
<input
className={css}
type="number"
min="0"
placeholder="Wert größer 0"
onChange={handleP_HZ}
value={P_HZ === -1 ? '' : P_HZ}
disabled={polygon ? false : true}
></input>
</InputSection>
<InputSection>
<label htmlFor="pkl-input">Kühlleistung in kW (optional)</label>
<input
className={css}
type="number"
min="0"
placeholder="Wert größer 0"
onChange={handleP_KL}
value={P_KL === -1 ? '' : P_KL}
disabled={polygon ? false : true}
></input>
</InputSection>
<InputSection>
<label htmlFor="bshz-input">Jahresbetriebsstunden Heizen (optional)</label>
<input
className={css}
type="number"
min="0"
max="4379"
placeholder="Wert zwischen 0 und 4379"
onChange={handleBS_HZ}
value={BS_HZ === -1 ? '' : BS_HZ}
disabled={polygon ? false : true}
></input>
</InputSection>
<InputSection>
<label htmlFor="bskl-input">Jahresbetriebsstunden Kühlen (optional)</label>
<input
type="number"
min="0"
max="4379"
placeholder="Wert zwischen 0 und 4379"
onChange={handleBS_KL}
value={BS_KL === -1 ? '' : BS_KL}
className={css}
disabled={polygon ? false : true}
></input>
</InputSection>
{(P_HZ > 0 || P_KL > 0 || BS_HZ > 0 || BS_KL > 0) &&
(P_HZ === -1 || P_KL === -1 || BS_HZ === -1 || BS_KL === -1) ? (
<Warning>Solange nicht alle Parameter ausgefüllt sind, wird mit Normwerten gerechnet.</Warning>
) : null}
{points.length > 300 && <Warning>Es sind maximal 300 Punkte möglich. Bitte löschen Sie zuerst Punkte.</Warning>}
{polygon && points.length === 0 ? <Warning>Zeichnen Sie mindestens einen Punkt!</Warning> : null}
<div className=" p-px w-full">
<button
onClick={handlePythonCalculation}
className={
polygon && points.length > 0 && points.length <= 300
? 'w-full h-10 border-none bg-gray-700 hover:bg-gray-500 text-white mb-2 py-2 px-4 cursor-pointer'
: 'w-full h-10 border-none bg-gray-700 hover:bg-gray-500 text-white mb-2 py-2 px-4 opacity-50 cursor-not-allowed'
}
disabled={polygon && points.length > 0 && points.length <= 300 ? false : true}
>
Berechnung starten
</button>
</div>
</div>
</div>
);
});

View File

@ -0,0 +1,68 @@
.esri-component {
max-height: 80%;
}
.esri-ui-corner .esri-component.esri-layer-list.esri-widget.esri-widget--panel {
max-height: 25%;
min-height: 45px;
}
.esri-component.esri-legend.esri-widget.esri-widget--panel {
max-height: 25%;
min-height: 45px;
}
.esri-ui-top-left.esri-ui-corner {
height: 95%;
}
.esri-ui-bottom-right.esri-ui-corner {
height: 95%;
}
a.nav-link {
color: #444444;
display: flex;
align-items: center;
text-decoration: none;
padding: 0 1rem;
height: fit-content;
cursor: pointer;
}
a.nav-link.active span,
a.nav-link:active,
a.nav-link:hover {
text-underline-offset: 3px;
text-decoration: solid underline #a92a4e;
}
div.esri-sketch__panel {
justify-content: start;
}
calcite-action[label='Select feature'] {
display: none;
}
calcite-action[label='Feature auswählen'] {
display: none;
}
calcite-icon {
color: #a92a4e;
width: 16px;
height: 16px;
}
.esri-layer-list {
background-color: #fff;
}
.esri-search {
width: 300px;
}
.esri-legend__layer-child-table > .esri-legend__layer-caption {
display: none;
}

View File

@ -0,0 +1,263 @@
import Graphic from '@arcgis/core/Graphic';
import * as geometryEngine from '@arcgis/core/geometry/geometryEngine';
import Point from '@arcgis/core/geometry/Point';
import Polygon from '@arcgis/core/geometry/Polygon';
import { view, pointGraphicsLayer, boundaryGraphicsLayer } from './potenzialkarte';
// grid points have to be at least 2.5 meters away from parcel boundary
const distanceToBoundary = 2.5;
const drawPoint = (point, color = [255, 255, 255, 0.25]) => {
pointGraphicsLayer.add(
new Graphic({
geometry: point,
symbol: {
type: 'simple-marker',
color,
},
})
);
};
const drawPolygon = (polygon) => {
boundaryGraphicsLayer.add(
new Graphic({
geometry: polygon,
symbol: {
type: 'simple-fill',
color: [0, 0, 0, 0],
outline: { color: '#009696', width: '2px' },
},
})
);
};
const dotProduct = (u, v) => {
return u[0] * v[0] + u[1] * v[1];
};
const determinant = (m) =>
m.length === 1
? m[0][0]
: m.length === 2
? m[0][0] * m[1][1] - m[0][1] * m[1][0]
: m[0].reduce(
(sum, element, i) =>
sum + (-1) ** (i + 2) * element * determinant(m.slice(1).map((c) => c.filter((_, j) => i !== j))),
0
);
const spread = (u, v) => {
return Math.pow(determinant([u, v]), 2) / (dotProduct(u, u) * dotProduct(v, v));
};
const intersect = (x1, y1, x2, y2, x3, y3, x4, y4) => {
// Check if no line is of length 0
if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
return false;
}
const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
// Lines are parallel
if (denominator === 0) {
return false;
}
// Return an object with the x and y coordinates of the intersection
let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
let x = x1 + ua * (x2 - x1);
let y = y1 + ua * (y2 - y1);
return [x, y];
};
const computeParallelLine = (line, offset) => {
const v = [line[1][0] - line[0][0], line[1][1] - line[0][1]];
const n = [-v[1], v[0]];
const il = 1 / Math.sqrt(Math.pow(n[0], 2) + Math.pow(n[1], 2));
const nn = [il * n[0], il * n[1]];
const p = [line[0][0] - offset * nn[0], line[0][1] - offset * nn[1]];
const q = [p[0] + v[0], p[1] + v[1]];
return [p, q];
};
const computeParallelLines = (line, offset, maxOffset) => {
const lines = [];
let currentOffset = offset;
while (Math.abs(currentOffset) < maxOffset) {
const offsetPolyline = computeParallelLine(line, currentOffset);
lines.push(offsetPolyline);
currentOffset += offset;
}
return lines;
};
export const distance = (point1, point2) => {
return Math.sqrt(Math.pow(point2[0] - point1[0], 2) + Math.pow(point2[1] - point1[1], 2));
};
const computeGridLines = (point1, point2, points, gridSpacing) => {
let v = [point2[0] - point1[0], point2[1] - point1[1]];
let n = [-v[0], -v[1]];
let c = -n[0] * point1[0] - n[1] * point1[1];
let longestDistanceRight = 0;
let longestDistanceLeft = 0;
for (let point of points) {
let projectedPoint = [
(point[0] * n[1] * n[1] - n[0] * n[1] * point[1] - n[0] * c) / (n[0] * n[0] + n[1] * n[1]),
(n[0] * n[0] * point[1] - n[0] * n[1] * point[0] - n[1] * c) / (n[0] * n[0] + n[1] * n[1]),
];
const length = distance(point1, projectedPoint);
const m = [
[point1[0], point1[1], 1],
[point2[0], point2[1], 1],
[projectedPoint[0], projectedPoint[1], 1],
];
if (determinant(m) < 0 && length > longestDistanceRight) {
longestDistanceRight = length;
} else if (determinant(m) > 0 && length > longestDistanceLeft) {
longestDistanceLeft = length;
}
}
const line = [point1, point2];
const linesRight = computeParallelLines([point1, point2], gridSpacing, longestDistanceRight);
const linesLeft = computeParallelLines([point1, point2], -gridSpacing, longestDistanceLeft);
const lines = linesRight.concat(linesLeft, [line]);
return lines;
};
export const calculateGrid = (polygon, gridSpacing = 10, setPoints) => {
boundaryGraphicsLayer.removeAll();
let offsetPolygon = geometryEngine.offset(polygon, distanceToBoundary, 'meters');
if (offsetPolygon) {
// draw only outer polygon and ignore inner rings
drawPolygon(
new Polygon({
rings: offsetPolygon.rings[0],
spatialReference: view.spatialReference,
})
);
const points = offsetPolygon.rings[0];
// search for most perpendicular corner
let widestSpread = 0;
let index = 0;
for (let i = 0; i < points.length - 1; i++) {
let currentSpread, u, v;
if (i === 0) {
u = [points[points.length - 1][0] - points[0][0], points[points.length - 1][1] - points[0][1]];
v = [points[1][0] - points[0][0], points[1][1] - points[0][1]];
currentSpread = spread(u, v);
} else if (i === points.length - 1) {
u = [
points[points.length - 2][0] - points[points.length - 1][0],
points[points.length - 2][1] - points[points.length - 1][1],
];
v = [points[points.length - 1][0] - points[0][0], points[points.length - 1][1] - points[0][1]];
currentSpread = spread(u, v);
} else {
u = [points[i - 1][0] - points[i][0], points[i - 1][1] - points[i][1]];
v = [points[i + 1][0] - points[i][0], points[i + 1][1] - points[i][1]];
currentSpread = spread(u, v);
}
if (currentSpread > widestSpread) {
widestSpread = currentSpread;
index = i;
}
}
const v = [points[index + 1][0] - points[index][0], points[index + 1][1] - points[index][1]];
const n = [-v[1], v[0]];
const lines = computeGridLines(points[index], points[index + 1], points, gridSpacing);
const orthogonalLines = computeGridLines(
points[index],
[points[index][0] + n[0], points[index][1] + n[1]],
points,
gridSpacing
);
const gridPoints = [];
for (let line1 of lines) {
for (let line2 of orthogonalLines) {
const intersection = intersect(
line1[0][0],
line1[0][1],
line1[1][0],
line1[1][1],
line2[0][0],
line2[0][1],
line2[1][0],
line2[1][1]
);
if (intersection) {
const intersectionPoint = new Point({
x: intersection[0],
y: intersection[1],
spatialReference: view.spatialReference,
});
gridPoints.push(intersectionPoint);
}
}
}
// filter points that are inside the boundary polygon
const filteredGridPoints = gridPoints.filter((point) => {
let include = false;
if (geometryEngine.intersects(point, offsetPolygon)) {
include = true;
}
return include;
});
// filter points that are not on buildings
filterPointsByPixelAndDraw(filteredGridPoints, setPoints);
}
};
// select points that are not on buildings
const filterPointsByPixelAndDraw = (points, setPoints) => {
view.takeScreenshot().then((screenshot) => {
let imageElement = document.createElement('img');
imageElement.src = screenshot.dataUrl;
const canvas = document.createElement('canvas');
canvas.width = view.width;
canvas.height = view.height;
const context = canvas.getContext('2d');
imageElement.onload = () => {
context.drawImage(imageElement, 0, 0);
const selectedGridPoints = [];
for (const point of points) {
const screenPoint = view.toScreen(point);
if (screenPoint) {
const { data } = context.getImageData(Math.round(screenPoint.x), Math.round(screenPoint.y), 1, 1);
// buildings are identified by their RGB values; G === B
if (!(data[1] === data[2])) {
selectedGridPoints.push(point);
}
}
}
// draw points
pointGraphicsLayer.removeAll();
selectedGridPoints.map((point) => drawPoint(point));
// set grid points for the UI
setPoints(selectedGridPoints.map((point) => [point.x, point.y]));
};
});
};

View File

@ -0,0 +1,9 @@
'use client';
import dynamic from 'next/dynamic';
const MapComponent = dynamic(() => import('./potenzialkarte'), { ssr: false });
export default function Potenzialkarte() {
return <MapComponent></MapComponent>;
}

View File

@ -0,0 +1,617 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { useMediaQuery } from 'react-responsive';
import Image from 'next/image';
import { updateAmpelkarteEWS } from '@/redux/ampelkarteEWSSlice';
import { updateResourcesEWS } from '@/redux/resourcesEWSSlice';
import { updateCadastralData } from '@/redux/cadastreSlice';
import { updateComputationResultEWS } from '@/redux/computationsEWSSlice';
import { useAppSelector, useAppDispatch } from '@/redux/hooks';
import { initializeInfoPanelHandlers } from './potenzialkarte';
import { TableAmpelkarteEWS } from '@/app/ews/table-ampelkarte-ews';
import Collapsible from '@/app/components/collapsible';
import Footer from '@/app/components/footer';
import Warning from '@/app/components/warning';
import Success from '@/app/components/success';
import { textTemplates } from '@/app/config/config';
import print from '@/app/utils/print';
const tableDataCSS = 'w-full break-words border-b border-solid border-gray-300';
export default function Panel() {
const image = useRef<HTMLImageElement>(null);
const image_bal = useRef<HTMLImageElement>(null);
const image_borefield = useRef<HTMLImageElement>(null);
const innerRef = useRef<HTMLDivElement | null>(null);
const [address, setAddress] = useState<string[]>([]);
const [closenessWarning, setClosenessWarning] = useState<boolean>(false);
const [outsideWarning, setOutsideWarning] = useState<boolean>(false);
const [scaleWarning, setScaleWarning] = useState<boolean>(true);
const [opened, setOpened] = useState<boolean>(true);
const ampelkarte: any = useAppSelector((store) => store.ampelkarteEWS.value);
const resources: any = useAppSelector((store) => store.resourcesEWS.value);
const cadastralData: any = useAppSelector((store) => store.cadastre.value);
const computationResult: any = useAppSelector((store) => store.computationsEWS.value);
const screenshot: any = useAppSelector((store) => store.screenshot.value);
const dispatch = useAppDispatch();
const isTablet = useMediaQuery({ maxWidth: 1024 });
// initialize query handlers
useEffect(() => {
dispatch(updateAmpelkarteEWS([]));
dispatch(updateResourcesEWS([]));
dispatch(updateCadastralData({}));
dispatch(updateComputationResultEWS({}));
initializeInfoPanelHandlers(setAddress, setClosenessWarning, setOutsideWarning, setScaleWarning);
return () => {
setAddress([]);
dispatch(updateAmpelkarteEWS([]));
dispatch(updateResourcesEWS([]));
dispatch(updateCadastralData({}));
dispatch(updateComputationResultEWS({}));
};
}, [dispatch, isTablet]);
// print pdf report
const clickHandler = () => {
print(
true,
true,
Object.keys(computationResult).length > 0,
screenshot,
image_bal,
image,
Object.keys(cadastralData).length > 0,
closenessWarning || outsideWarning ? true : false,
image_borefield,
computationResult.calculationMode,
'EWS',
Object.keys(resources).length > 0
);
};
// format values
const formatEWS = (layerId: number, layerName: string, value: string) => {
if (value !== 'NoData') {
if ([0, 1, 2, 4, 5, 6].includes(layerId)) {
value = parseFloat(value).toFixed(1);
} else {
value = parseFloat(value).toFixed(0);
}
return (textTemplates as any)[layerId][0] + value + (textTemplates as any)[layerId][1];
} else {
return layerName + ': keine Daten';
}
};
const handleClick = () => {
setOpened(!opened);
if (innerRef && innerRef.current) {
innerRef.current.classList.toggle('hidden');
}
};
return (
<div
className={`absolute top-4 right-4 ${opened && isTablet ? 'bottom-4' : ''} ${
opened && Object.keys(computationResult).length > 1 ? 'bottom-4' : ''
} w-full md:w-1/3 xl:w-1/4 pl-8 md:pl-0 z-50`}
>
<div
onClick={handleClick}
className="bg-gray-700 text-white cursor-pointer p-4 w-full border-0 hover:bg-gray-500 h-12 items-center justify-between text-sm xl:text-base"
>
Berechnungsergebnis <span className="float-right">{opened ? '-' : '+'}</span>
</div>
<div
className={`h-[calc(100%-3rem)] px-2 xl:px-4 py-4 overflow-y-auto bg-white text-sm ${opened ? '' : 'hidden'}`}
ref={innerRef}
>
<div className="flex justify-end">
<button
className={
ampelkarte && ampelkarte.length > 0
? 'text-white bg-red-700 p-2 border-0 outline-none cursor-pointer hover:bg-red-400 transition-colors mb-2 w-1/2 text-sm xl:text-base'
: 'text-white bg-red-500 p-2 border-0 outline-none cursor-not-allowed hover:bg-red-200 transition-colors mb-2 w-1/2 text-sm xl:text-base'
}
onClick={clickHandler}
>
PDF erstellen
</button>
</div>
{Object.keys(cadastralData).length > 0 ? (
<table id="cadastral-data-table" className="table-fixed w-full p-2">
<tbody>
<tr>
<td>
Katastralgemeinde: {cadastralData.KG}
<br></br>
Grundstücksnummer: {cadastralData.GNR}
</td>
</tr>
</tbody>
</table>
) : null}
{address && address.length > 0 ? (
<table id="address-table" className="table-fixed w-full p-2">
<tbody>
<tr>
<td>{address[0]}</td>
</tr>
<tr>
<td>
{address[1]} {address[3]}
</td>
</tr>
</tbody>
</table>
) : null}
{Object.keys(cadastralData).length > 0 ? (
<div className="grid grid-cols-3 p-2">
<span className="border border-solid border-blue-700 my-auto mr-2"></span>
<span className="col-span-2">Grundstücksgrenze</span>
<span className="border border-solid border-teal-600 my-auto mr-2"></span>
<span className="col-span-2">2,5-Meter-Abstand zur Grundstücksgrenze</span>
</div>
) : null}
{scaleWarning ? (
Object.keys(cadastralData).length === 0 ? (
<div className="py-2">
<Warning>Bitte zoomen Sie hinein um Ihr gewünschtes Grundstück durch Mausklick auszuwählen.</Warning>
</div>
) : (
<div className="py-2">
<Warning>Bitte zoomen Sie hinein, wenn Sie ein anderes Grundstück auswählen möchten.</Warning>
</div>
)
) : Object.keys(cadastralData).length === 0 ? (
<div className="py-2">
<Success>Sie können jetzt Ihr Grundstück auswählen.</Success>
</div>
) : (
<div className="py-2">
<Success>Sie können jetzt die Berechnungen starten oder ein anderes Grundstück auswählen.</Success>
</div>
)}
{closenessWarning || outsideWarning ? (
<table id="warnings-table" className="table-fixed w-full mb-2">
<tbody>
<tr>
<td>
{closenessWarning ? (
<Warning>Achtung: Mindestens ein Punkt liegt näher als fünf Meter zu einem anderen Punkt!</Warning>
) : null}
</td>
</tr>
<tr>
<td>
{outsideWarning ? (
<Warning>Achtung: Mindestens ein Punkt liegt außerhalb der zugelassenen Grenzen!</Warning>
) : null}
</td>
</tr>
</tbody>
</table>
) : null}
{ampelkarte && ampelkarte.length > 0 ? <TableAmpelkarteEWS results={ampelkarte}></TableAmpelkarteEWS> : null}
{Object.keys(computationResult).includes('error') ? (
<Collapsible title="Berechnungsergebnisse" open={true}>
<table id="calculations-output-table" className="table-fixed w-full p-2">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>
<tr>
<th>{computationResult.error}</th>
</tr>
</tbody>
</table>
<div></div>
</Collapsible>
) : null}
{resources && resources.length > 0 ? (
<div className="hidden">
<Collapsible title="Ressourcen" open={false}>
<table id="resources-table" className="table-fixed w-full p-2">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>
<tr>
<th>Ressourcen für vordefinierte Erdwärmesondenanlage</th>
</tr>
{resources.slice(3, 7).map((result: any) => {
return (
<tr key={result.layerId}>
<td className={tableDataCSS}>
{formatEWS(
result.layerId,
result.layerName,
result.feature.attributes['Classify.Pixel Value']
)}
</td>
</tr>
);
})}
<tr>
<th>Standortabhängige Parameter</th>
</tr>
{resources.slice(0, 3).map((result: any) => {
return (
<tr key={result.layerId}>
<td className={tableDataCSS}>
{formatEWS(
result.layerId,
result.layerName,
result.feature.attributes['Classify.Pixel Value']
)}
</td>
</tr>
);
})}
</tbody>
</table>
</Collapsible>
</div>
) : null}
{Object.keys(computationResult).length > 1 ? (
<Collapsible title="Berechnungsergebnisse" open={true}>
<table id="calculations-input-table" className="table-fixed w-full p-2">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>
<tr>
<th>Berechnungsvorgaben</th>
</tr>
<tr>
{computationResult.calculationMode === 'norm' ? (
<td>
Es wurde keine Betriebsfunktion vom Benutzer vorgeben. Die Berechnung der möglichen Leistung des
gewählten Sondenfelds erfolgt mit Norm-Jahresbetriebsstunden für Heizen und Kühlen am Standort
eines typischen Wohngebäudes. Die Berechnung berücksichtigt zudem Untergrunddaten, fixe
Sondenparameter und Temperaturgrenzen für die Fluidtemperatur in der Sonde. Ergebnisse sind die
maximal erzielbare Leistung (kW) und Energiemenge (MWh/a) bei einem Betrieb von 20 Jahren.
</td>
) : (
<td>
Die Berechnung erfolgt für das gewählte Sondenfeld mit der benutzerdefinierten Betriebsfunktion,
bestehend aus den Jahresbetriebsstunden und dem Leistungsverhältnis zwischen Heizen und Kühlen.
Die Berechnung berücksichtigt zudem Untergrunddaten, fixe Sondenparameter und Temperaturgrenzen
für die Fluidtemperatur in der Sonde. Ergebnisse sind die maximal erzielbare Leistung (kW) und
Energiemenge (MWh/a) bei einem Betrieb von 20 Jahren.
</td>
)}
</tr>
<tr>
<th>Benutzereingabe</th>
</tr>
<tr>
<td>Sondenanzahl: {computationResult.points}</td>
</tr>
{computationResult.meanBoreholeSpacing > 0 && (
<tr>
<td>Durchschnittlicher Sondenabstand: {computationResult.meanBoreholeSpacing} m</td>
</tr>
)}
<tr>
<td>Sondentiefe: {computationResult.boreDepth} m</td>
</tr>
{computationResult.calculationMode === 'user' && (
<>
<tr>
<td>Jahresbetriebsstunden Heizen: {computationResult.BS_HZ} h</td>
</tr>
<tr>
<td>Jahresbetriebsstunden Kühlen: {computationResult.BS_KL} h</td>
</tr>
<tr>
<td>Heizleistung Gebäude: {computationResult.P_HZ} kW</td>
</tr>
<tr>
<td>Kühlleistung Gebäude: {computationResult.P_KL} kW</td>
</tr>
</>
)}
<tr>
<td>Vorlauftemperatur Heizung: {computationResult.T_radiator} °C</td>
</tr>
<tr>
<th>Standortabhängige Parameter</th>
</tr>
{computationResult.calculationMode === 'norm' && (
<>
<tr>
<td>Norm-Jahresbetriebsstunden Heizen: {computationResult.BS_HZ_Norm} h</td>
</tr>
<tr>
<td>Norm-Jahresbetriebsstunden Kühlen: {computationResult.BS_KL_Norm} h</td>
</tr>
</>
)}
{resources && resources.length > 0 && computationResult && computationResult.GTcalc && (
<>
<tr>
<td>
Wärmeleitfähigkeit:{' '}
{parseFloat(resources[2].feature.attributes['Classify.Pixel Value']).toFixed(1)} W/m/K
</td>
</tr>
<tr>
<td>Untergrundtemperatur: {computationResult.GTcalc.toFixed(1)} °C</td>
</tr>
</>
)}
{computationResult.points >= 10 &&
(computationResult.Efactor_user > 1.1 || computationResult.Efactor_user < 0.9) && (
<tr>
<td>
Hinweis: Größere Sondenfelder sollten mit einer möglichst ausgeglichenen Jahresenergiebilanz
zwischen Heizen und Kühlen betrieben werden. Dadurch beeinflussen sich die Sonden gegenseitig
kaum und der Sondenabstand kann auf ungefähr 5 Meter reduziert werden. Dies ermöglicht eine
optimale thermische Nutzung des Untergrunds und es können höhere Leistungen erreicht werden.
Überlegen Sie eine Verbesserung der Energiebilanz zwischen Heizen und Kühlen!
</td>
</tr>
)}
</tbody>
</table>
<div></div>
<Image
src={computationResult.imagehashSondenfeld}
alt="Grafik mit Sondenfeld"
width={384}
height={288}
ref={image_borefield}
className="mx-auto"
></Image>
<table id="calculations-output-table" className="table-fixed w-full p-2">
<tbody>
<tr>
<th>
{computationResult.calculationMode === 'norm'
? 'Berechnungsergebnisse für den Normbetrieb'
: 'Berechnungsergebnisse für den benutzerdefinierten Betrieb '}
</th>
</tr>
<tr>
<th>
Heizbetrieb mit{' '}
{computationResult.BS_HZ > 0 ? computationResult.BS_HZ : computationResult.BS_HZ_Norm} h/a
</th>
</tr>
<tr>
<td>Wärmeentzugsleistung aus Erdwärmesonden: {computationResult.P_HZ_user.toFixed(1)} kW</td>
</tr>
<tr>
<td>
+ Elektrische Leistung Wärmepumpe (bei COP {computationResult.COP.toFixed(1)}):{' '}
{computationResult.Pel_heatpump_user.toFixed(1)} kW
</td>
</tr>
<tr>
<td>= Heizleistung Erdwärmeanlage: {computationResult.heizleistung.toFixed(1)} kW</td>
</tr>
<tr>
<td>Jährlicher Wärmeentzug aus Erdwärmesonden: {computationResult.E_HZ_user.toFixed(1)} MWh/a</td>
</tr>
<tr>
<td>
+ Strombedarf Wärmepumpe (bei JAZ {computationResult.SCOP.toFixed(1)}):{' '}
{computationResult.Eel_heatpump_user.toFixed(1)} MWh/a
</td>
</tr>
<tr>
<td>= Heizarbeit Erdwärmeanlage: {computationResult.heizarbeit.toFixed(1)} MWh/a</td>
</tr>
<tr>
<th>
Kühlbetrieb mit{' '}
{computationResult.BS_KL > 0 ? computationResult.BS_KL : computationResult.BS_KL_Norm} h/a
</th>
</tr>
<tr>
<td>Wärmeeintragsleistung in Erdwärmesonden: {computationResult.P_KL_user.toFixed(1)} kW</td>
</tr>
<tr>
<td>
- Elektrische Leistung Wärmepumpe (bei EER {computationResult.EER.toFixed(1)}):{' '}
{computationResult.Pel_chiller_user.toFixed(1)} kW
</td>
</tr>
<tr>
<td>= Kühlleistung Erdwärmeanlage: {computationResult.kuehlleistung.toFixed(1)} kW</td>
</tr>
<tr>
<td>Jährlicher Wärmeeintrag in Erdwärmesonde: {computationResult.E_KL_user.toFixed(1)} MWh/a</td>
</tr>
<tr>
<td>
- Strombedarf Wärmepumpe (bei SEER {computationResult.SEER.toFixed(1)}
): {computationResult.Eel_chiller_user.toFixed(1)} MWh/a
</td>
</tr>
<tr>
<td>= Kühlarbeit Erdwärmeanlage: {computationResult.kuehlarbeit.toFixed(1)} MWh/a</td>
</tr>
{computationResult.cover > 0 && (
<tr>
<td>Deckungsgrad gesamt: {computationResult.cover} %</td>
</tr>
)}
{computationResult.balanced === 0 && (
<tr>
<td>
Ihre Energie- und Leistungsvorgaben des Gebäudes für Heizen und Kühlen bewirken eine ausgeglichene
Betriebsweise im Erdsondenfeld. Die Auslegung ist optimal für den saisonalen Speicherbetrieb
geeignet.
</td>
</tr>
)}
</tbody>
</table>
<div></div>
<Image
src={computationResult.imagehash}
alt="Grafik mit Berechnungsergebnissen"
ref={image}
width={384}
height={288}
className="mx-auto"
></Image>
<div></div>
</Collapsible>
) : null}
{Object.keys(computationResult).length > 1 && computationResult.balanced === 1 ? (
<Collapsible title="Berechnungsergebnisse für den saisonalen Speicherbetrieb" open={true}>
<table id="calculations-bal-output-table" className="table-fixed w-full p-2">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>
<tr>
<td>
Die Berechnung erfolgt für das gewählte Sondenfeld im saisonalen Speicherbetrieb. Die folgenden
Ergebnisse beziehen sich auf eine automatisch angepasste Betriebsweise, wobei das Sondenfeld über
das Jahr gesehen gleich stark be- und entladen wird.{' '}
{computationResult.Efactor_user >= 1 &&
`Um eine ausgeglichene Betriebsweise zu erreichen ist eine zusätzliche Wärmequelle notwendig. Die Zusatzquelle (Solar, Luft, Abwärme, etc.) kann außerhalb der Heizsaison betrieben werden und gleicht die Jahresenergiebilanz der Erdwärmesonden aus. Alternativ zur Zusatzquelle kann auch eine Reduktion der vorgegebenen Heizleistung mit klassischen Wärmequellen (Fernwärme, Biomasse, etc.) in Betracht gezogen werden. Versuchen Sie auch eine manuelle Anpassung der Leistungsvorgabe!`}
{computationResult.Efactor_user < 1 &&
`Um eine ausgeglichene Betriebsweise zu erreichen ist eine zusätzliche Wärmesenke notwendig. Als Zusatzsenke bietet sich eine Wärmeversorgung benachbarter Objekte an, wodurch die Jahresenergiebilanz in den Erdwärmesonden ausgeglichen werden kann.`}
</td>
</tr>
{computationResult.meanBoreholeSpacing > 5 && (
<>
<tr>
<td>
Hinweis: Im saisonalen Speicherbetrieb kann der Sondenabstand auf ungefähr fünf Meter reduziert
werden ohne dass sich die einzelnen Erdwärmesonden nennenswert gegenseitig beeinflussen. Somit
kann der Flächenbedarf ohne signifikante Einbußen der Sondenleistung reduziert werden. Versuchen
Sie eine Reduktion des Sondenabstands!
</td>
</tr>
</>
)}
</tbody>
<tbody>
<tr>
<th>Berechnungsergebnisse</th>
</tr>
<tr>
<th>
Heizbetrieb {computationResult.Efactor_user < 1 && 'und Zusatzsenke'} mit{' '}
{computationResult.BS_HZ_bal} h/a
</th>
</tr>
<tr>
<td>Wärmeentzugsleistung aus Erdwärmesonden: {computationResult.P_HZ_bal.toFixed(1)} kW</td>
</tr>
<tr>
<td>
+ Elektrische Leistung Wärmepumpe (bei COP {computationResult.COP_bal.toFixed(1)}):{' '}
{computationResult.Pel_heatpump_bal.toFixed(1)} kW
</td>
</tr>
<tr>
<td>= Heizleistung Erdwärmeanlage: {computationResult.heizleistungBal.toFixed(1)} kW</td>
</tr>
<tr>
<td>Jährlicher Wärmeentzug aus Erdwärmesonden: {computationResult.E_HZ_bal.toFixed(1)} MWh/a</td>
</tr>
<tr>
<td>
+ Strombedarf Wärmepumpe (bei JAZ {computationResult.SCOP_bal.toFixed(1)}):{' '}
{computationResult.Eel_heatpump_bal.toFixed(1)} MWh/a
</td>
</tr>
<tr>
<td>= Heizarbeit Erdwärmeanlage: {computationResult.heizarbeitBal.toFixed(1)} MWh/a</td>
</tr>
<tr>
<th>
Kühlbetrieb {computationResult.Efactor_user >= 1 && 'und Zusatzquelle'} mit{' '}
{computationResult.BS_KL_bal} h/a
</th>
</tr>
<tr>
<td>Wärmeeintragsleistung in Erdwärmesonden: {computationResult.P_KL_bal.toFixed(1)} kW</td>
</tr>
<tr>
<td>
- Elektrische Leistung Wärmepumpe (bei EER {computationResult.EER_bal.toFixed(1)}):{' '}
{computationResult.Pel_chiller_bal.toFixed(1)} kW
</td>
</tr>
<tr>
<td>= Kühlleistung Erdwärmeanlage: {computationResult.kuehlleistungBal.toFixed(1)} kW</td>
</tr>
<tr>
<td>Jährlicher Wärmeeintrag in Erdwärmesonden: {computationResult.E_KL_bal.toFixed(1)} MWh/a</td>
</tr>
<tr>
<td>
- Strombedarf Wärmepumpe (bei SEER {computationResult.SEER_bal.toFixed(1)}):{' '}
{computationResult.Eel_chiller_bal.toFixed(1)} MWh/a
</td>
</tr>
<tr>
<td>= Kühlarbeit Erdwärmeanlage: {computationResult.kuehlarbeitBal.toFixed(1)} MWh/a</td>
</tr>
{computationResult.cover > 0 && (
<tr>
<td>Deckungsgrad: {computationResult.cover_bal} %</td>
</tr>
)}
{computationResult.Efactor_user >= 1 ? (
<tr>
<td>
Bei einer ausgeglichenen Betriebsweise mit einer zusätzlichen Wärmequelle kann die Heizarbeit um{' '}
{computationResult.cover_rise.toFixed(1)} % gesteigert werden.
</td>
</tr>
) : (
<tr>
<td>
Bei einer ausgeglichenen Betriebsweise mit einer zusätzlichen Wärmesenke kann die Kühlarbeit um{' '}
{computationResult.cover_rise.toFixed(1)} % gesteigert werden.
</td>
</tr>
)}
</tbody>
</table>
<div></div>
<Image
src={computationResult.imagehashBal}
alt="Grafik mit bilanzierten Berechnungsergebnissen"
ref={image_bal}
width={384}
height={288}
className="mx-auto"
></Image>
<div></div>
</Collapsible>
) : null}
<Footer></Footer>
</div>
</div>
);
}

View File

@ -0,0 +1,487 @@
import { useRef, useEffect, useState, Dispatch, SetStateAction } from 'react';
import { useMediaQuery } from 'react-responsive';
import ArcGISMap from '@arcgis/core/Map';
import Extent from '@arcgis/core/geometry/Extent';
import MapView from '@arcgis/core/views/MapView';
import Basemap from '@arcgis/core/Basemap';
import MapImageLayer from '@arcgis/core/layers/MapImageLayer';
import WMTSLayer from '@arcgis/core/layers/WMTSLayer';
import Search from '@arcgis/core/widgets/Search';
import ScaleBar from '@arcgis/core/widgets/ScaleBar';
import Sketch from '@arcgis/core/widgets/Sketch';
import GraphicsLayer from '@arcgis/core/layers/GraphicsLayer';
import Graphic from '@arcgis/core/Graphic';
import * as geometryEngine from '@arcgis/core/geometry/geometryEngine';
import * as reactiveUtils from '@arcgis/core/core/reactiveUtils';
import Zoom from '@arcgis/core/widgets/Zoom';
import FeatureLayer from '@arcgis/core/layers/FeatureLayer';
import Polygon from '@arcgis/core/geometry/Polygon';
import SimpleFillSymbol from '@arcgis/core/symbols/SimpleFillSymbol';
import SimpleMarkerSymbol from '@arcgis/core/symbols/SimpleMarkerSymbol';
import esriConfig from '@arcgis/core/config';
import Point from '@arcgis/core/geometry/Point';
import { updateComputationResultEWS } from '@/redux/computationsEWSSlice';
import { updateCadastralData } from '@/redux/cadastreSlice';
import identifyAllLayers from '../../utils/identify';
import queryCadastre from '../../utils/queryCadastre';
import getAddress from '../../utils/getAddress';
import './esri-ui-potenzialkarte.css';
import { Vienna, Austria } from '@/public/borders-vienna-austria';
import ReduxProvider from '@/redux/provider';
import { useAppDispatch } from '@/redux/hooks';
import PanelPotenzialkarte from './panel-potenzialkarte';
import CalculationsMenu from './calculations-menu';
// import layer URLs and spatial reference system wkid
import { BASEMAP_AT_URL, AMPEL_EWS_URL, RESOURCES_EWS_URL, SRS } from '@/app/config/config';
// set path for local assets
esriConfig.assetsPath = '/assets';
const scaleLimit = 1000;
const gridSpacing = 10;
// initialize info panel callback functions
let setAddress: Dispatch<SetStateAction<string[]>>,
setClosenessWarning: Dispatch<SetStateAction<boolean>>,
setOutsideWarning: Dispatch<SetStateAction<boolean>>,
setScaleWarning: Dispatch<SetStateAction<boolean>>;
export function initializeInfoPanelHandlers(
setAddressCallback: Dispatch<SetStateAction<string[]>>,
setClosenessWarningCallback: Dispatch<SetStateAction<boolean>>,
setOutsideWarningCallback: Dispatch<SetStateAction<boolean>>,
setScaleWarningCallback: Dispatch<SetStateAction<boolean>>
) {
setAddress = setAddressCallback;
setClosenessWarning = setClosenessWarningCallback;
setOutsideWarning = setOutsideWarningCallback;
setScaleWarning = setScaleWarningCallback;
}
// initialize calculations menu callback functions
let setPoints: Dispatch<SetStateAction<number[][]>>, setPolygon: Dispatch<SetStateAction<Polygon | null>>;
export function initializeCalculationsMenuHandlers(
setPointsCallback: Dispatch<SetStateAction<number[][]>>,
setPolygonCallback: Dispatch<SetStateAction<Polygon | null>>
) {
setPoints = setPointsCallback;
setPolygon = setPolygonCallback;
}
// initialize the map view container
export let view: MapView;
export let pointGraphicsLayer: GraphicsLayer;
export let boundaryGraphicsLayer: GraphicsLayer;
export default function MapComponent() {
const dispatch = useAppDispatch();
const isMobile = useMediaQuery({ maxWidth: 640 });
const mapDiv = useRef<HTMLDivElement | null>(null);
const calculationsMenuRef = useRef<HTMLDivElement | null>(null);
const loaded = useRef<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
let handle: IHandle;
if (mapDiv.current && calculationsMenuRef.current) {
view = new MapView({
container: mapDiv.current,
extent: new Extent({
xmin: -19000,
ymin: 325000,
xmax: 29000,
ymax: 360000,
spatialReference: { wkid: SRS },
}),
spatialReference: { wkid: SRS },
popupEnabled: false,
popup: {
dockEnabled: false,
dockOptions: {
buttonEnabled: false,
},
collapseEnabled: false,
viewModel: {
includeDefaultActions: false,
},
},
});
// listen to scale changes
handle = reactiveUtils.watch(
() => view.scale,
() => {
if (view.scale < scaleLimit) {
setScaleWarning(false);
} else {
setScaleWarning(true);
}
}
);
// graphic layers
let viennaGraphic = new Graphic({
geometry: new Polygon({ rings: [Austria, Vienna], spatialReference: { wkid: SRS } }),
symbol: {
type: 'simple-fill',
color: [209, 213, 219, 0.65],
style: 'solid',
outline: {
color: 'white',
width: 0,
},
} as unknown as SimpleFillSymbol,
});
const viennaGraphicsLayer = new GraphicsLayer({ title: 'Wien', listMode: 'hide' });
viennaGraphicsLayer.add(viennaGraphic);
pointGraphicsLayer = new GraphicsLayer({
title: 'Planungslayer - Punkte',
listMode: 'hide',
});
boundaryGraphicsLayer = new GraphicsLayer({
title: 'EWS - innere Grenze',
listMode: 'hide',
});
const polygonGraphicsLayer = new GraphicsLayer({
title: 'Grundstücksgrenze',
listMode: 'hide',
});
const highlightGraphicsLayer = new GraphicsLayer({
title: 'Grundstücksgrenze',
listMode: 'hide',
});
// thematic layers
const ampelkarte_ews = new FeatureLayer({
url: AMPEL_EWS_URL + '/0',
title: 'Mögliche Einschränkungen',
visible: false,
listMode: 'show',
opacity: 0.5,
});
const ews = new MapImageLayer({
title: 'Ressourcen',
url: RESOURCES_EWS_URL,
visible: false,
listMode: 'show',
});
// basemap in Viennese coordinate system due to tranformation inaccuracies from MGI to WGS84
// default transformation in ArcGIS API from MGI to WGS84 is 1306
// transformation 1618 is recommended
const basemap_at = new WMTSLayer({
url: BASEMAP_AT_URL,
activeLayer: {
id: 'geolandbasemap',
},
listMode: 'hide',
serviceMode: 'KVP',
});
let basemap = new Basemap({
baseLayers: [basemap_at],
title: 'basemap',
id: 'basemap',
spatialReference: { wkid: SRS },
});
let arcgisMap = new ArcGISMap({
basemap: basemap,
layers: [
ews,
ampelkarte_ews,
pointGraphicsLayer,
boundaryGraphicsLayer,
polygonGraphicsLayer,
highlightGraphicsLayer,
viennaGraphicsLayer,
],
});
const search = new Search({
view,
popupEnabled: true,
});
const scaleBar = new ScaleBar({
view: view,
unit: 'metric',
});
const zoom = new Zoom({
view,
layout: 'horizontal',
});
let sketch = new Sketch({
// container: calculationsMenuRef.current,
layer: pointGraphicsLayer,
view: view,
availableCreateTools: ['point'],
visibleElements: {
selectionTools: {
'lasso-selection': true,
'rectangle-selection': true,
},
settingsMenu: false,
undoRedoMenu: false,
},
creationMode: 'single',
});
if (loaded.current) {
sketch.container = calculationsMenuRef.current;
}
loaded.current = true;
sketch.on('create', (event) => {
if (event.tool === 'point' && event.state === 'complete') {
// point symbol
const symbol = {
type: 'simple-marker',
color: [255, 255, 255, 0.25],
};
// delete default point symbol
pointGraphicsLayer.remove(event.graphic);
let point = event.graphic.geometry as Point;
let points;
// add point to current list of points
setPoints((currentPoints) => {
points = [...currentPoints, [point.x, point.y]];
return points;
});
// check if point is too close to any other point
pointGraphicsLayer.graphics.forEach((graphic) => {
if (geometryEngine.distance(graphic.geometry, point) < 5) {
setClosenessWarning(true);
}
});
// check if point is outside the parcel
if (!geometryEngine.intersects(point, boundaryGraphicsLayer.graphics.at(0).geometry)) {
setOutsideWarning(true);
}
// draw point graphic
pointGraphicsLayer.add(
new Graphic({
geometry: point,
symbol,
})
);
}
});
sketch.on('update', (event) => {
if (event.state === 'start') {
// on first load update starts with two identical points
// cancel second update
if (sketch.updateGraphics.length === 2 && sketch.updateGraphics.at(0) === sketch.updateGraphics.at(1)) {
sketch.cancel();
}
}
if (event.state === 'complete') {
// list of updated points
let allPoints = pointGraphicsLayer.graphics.map((graphic) => graphic.geometry as Point);
// update state of calculations menu
setPoints(allPoints.map((point) => [point.x, point.y]).toArray());
// check if all points are inside the boundary
let allPointsInside = true;
allPoints.forEach((point) => {
if (!geometryEngine.intersects(point, boundaryGraphicsLayer.graphics.at(0).geometry)) {
allPointsInside = false;
return;
}
});
setOutsideWarning(!allPointsInside);
// check if points are too close
let tooClose = false;
allPoints.forEach((firstPoint) => {
allPoints.forEach((secondPoint) => {
if (secondPoint !== firstPoint) {
if (geometryEngine.distance(firstPoint, secondPoint, 'meters') <= 4.9) {
tooClose = true;
return;
}
}
});
});
setClosenessWarning(tooClose);
}
});
// register event handler for mouse clicks
view.on('click', ({ mapPoint }) => {
// at application start if no polygon is drawn yet
if (polygonGraphicsLayer.graphics.length === 0) {
startNewQuery(mapPoint);
} else {
// check if point is selected
view
.hitTest(view.toScreen(mapPoint), {
include: pointGraphicsLayer,
})
.then(({ results }) => {
if (results && results.length > 0) {
// if point was selected then start update
const pointGraphic = (results.at(0) as any)?.graphic;
sketch.update(pointGraphic);
} else {
if (
!geometryEngine.intersects(mapPoint, polygonGraphicsLayer.graphics.at(0).geometry) &&
view.scale < scaleLimit
) {
// if click was outside parcel and no point was selected then start new query
view.openPopup({
title: 'Wollen Sie ein neues Grundstück auswählen?',
location: mapPoint,
content: getPopupContent(mapPoint),
});
}
}
});
}
});
// listen to move event
view.on('pointer-move', (event) => {
let mapPoint = view.toMap({ x: event.x, y: event.y });
view
.hitTest(view.toScreen(mapPoint), {
include: pointGraphicsLayer,
})
.then(({ results }) => {
// if user hovers over a point
if (results.length > 0 && event.buttons === 0 && !sketch.createGraphic) {
const graphicHits = results?.filter((hitResult) => hitResult.type === 'graphic');
let graphic = (graphicHits.at(0) as any)?.graphic;
sketch.update(graphic);
let pointGraphic = new Graphic({
geometry: graphic.geometry,
symbol: {
type: 'simple-marker',
size: '30px',
color: null,
outline: { color: '#00ffff' },
} as unknown as SimpleMarkerSymbol,
});
highlightGraphicsLayer.add(pointGraphic);
document.body.style.cursor = 'pointer';
} else {
highlightGraphicsLayer.removeAll();
document.body.style.cursor = 'default';
}
});
});
const getPopupContent = (mapPoint: Point) => {
const popupContent = document.createElement('div');
popupContent.className = 'flex justify-center';
const popupButton = document.createElement('button');
popupButton.textContent = 'Ja';
popupButton.className = 'text-white bg-gray-700 p-2 border-none outline-none cursor-pointer w-2/3';
popupButton.addEventListener('mouseover', handleMouseOver);
function handleMouseOver(this: HTMLButtonElement) {
this.className = 'text-white bg-gray-300 p-2 border-none outline-none cursor-pointer w-2/3';
}
popupButton.addEventListener('mouseout', handleMouseOut);
function handleMouseOut(this: HTMLButtonElement) {
this.className = 'text-white bg-gray-700 p-2 border-none outline-none cursor-pointer w-2/3';
}
popupContent.append(popupButton);
popupButton.onclick = () => {
startNewQuery(mapPoint);
view.popup.close();
};
return popupContent;
};
const startNewQuery = (mapPoint: Point) => {
// remove existing graphics
polygonGraphicsLayer.removeAll();
boundaryGraphicsLayer.removeAll();
pointGraphicsLayer.removeAll();
// clear state
setClosenessWarning(false);
setOutsideWarning(false);
setPolygon(null);
setPoints([]);
// initialize store in case there was a previous computation
dispatch(updateCadastralData({}));
dispatch(updateComputationResultEWS({}));
// start new layer queries
identifyAllLayers(view, mapPoint, dispatch, 'EWS');
getAddress(mapPoint, setAddress);
if (view.scale < scaleLimit) {
// query cadastral data
queryCadastre(view, polygonGraphicsLayer, mapPoint, dispatch, setPolygon, setPoints, gridSpacing);
}
};
// add map to view
view.map = arcgisMap;
// add UI components
view.ui.components = [];
if (!isMobile) {
view.ui.add([zoom, search], 'top-left');
view.ui.add(scaleBar, 'bottom-left');
}
}
return () => {
view?.destroy();
handle?.remove();
};
}, [dispatch, isMobile]);
return (
<div ref={mapDiv} className="absolute top-16 bottom-0 w-full">
<PanelPotenzialkarte></PanelPotenzialkarte>
<ReduxProvider>
<CalculationsMenu isLoading={setLoading} ref={calculationsMenuRef}></CalculationsMenu>
</ReduxProvider>
{loading ? (
<div
className="absolute left-1/2 top-1/2 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-gray-950 border-r-transparent"
role="status"
></div>
) : null}
</div>
);
}

View File

@ -0,0 +1,300 @@
import Collapsible from '@/app/components/collapsible';
const tableDataCSS = 'w-full break-words border-b border-solid border-gray-300 px-2 text-sm';
let einschraenkungen_erlaeuterungen: any = {};
const einschraenkungenText: any = {
Naturschutz: `Sind durch ein Vorhaben ein Schutzgebiet (z.B. Nationalpark, Europaschutzgebiet, Landschaftsschutzgebiet, Naturschutzgebiet, geschützter Landschafsteil),
ein Schutzobjekt (Naturdenkmal) oder streng (geschützte) Tier- und Pflanzenarten betroffen, ist jedenfalls rechtzeitig mit der Magistratsabteilung 22 - Umweltschutz Kontakt aufzunehmen,
um eine allfällige naturschutzbehördliche Bewilligungspflicht abklären zu können. Auf folgenden Seiten sind sämtliche Informationen zu Schutzgebieten und objekten sowie zu den Artenschutzbestimmungen zu finden:`,
Naturschutz_links: [
'https://www.wien.gv.at/umweltschutz/naturschutz/gebiet/schutzgebiete.html#schutzgebiete',
'https://www.wien.gv.at/umweltschutz/naturschutz/biotop/artenschutz.html',
],
'Artesisch gespannte Brunnen': `In einem Umkreis von 100 m Radius wurde mit Bohrungen artesisch gespanntes Grundwasser angetroffen. Bei der Planung und Durchführung zukünftiger Bohrungen in diesem Bereich muss dies berücksichtigt werden.`,
Karstzonen: `Am Standort treten verkarstungsfähige Gesteine auf. Bohrungen können daher Hohlräume antreffen.`,
Altlasten: `Es befindet sich eine Altlast am Standort. Weitere Informationen über die Altlasten sind im Altlasten-GIS unter folgendem Link zu finden: `,
Altlasten_links: 'https://secure.umweltbundesamt.at/altlasten/?servicehandler=publicgis',
'Unterirdische Bauwerke': `In einem Umkreis von 2 m befindet sich ein unterirdisches Verkehrsbauwerk (entsprechend Digitalem Verkehrsgraph - GIP). Auf diesen Flächen und im Nahbereich ist der Einsatz von Erdwärmesonden ausgeschlossen.
Es kann jedoch noch weitere unterirdische Bauwerke wie Tiefgaragen, Verbindungsgänge oder Einbauten im restlichen Stadtgebiet geben, die den Einsatz von Erdwärmesonden beschränken können.`,
};
const hinweiseText: any = {
Grundwasserchemismus: {
'Eisen- und Manganausfällung': `Am Standort kann es zu Eisen- und Manganausfällungen in den Brunnen kommen.
Diese können mit bestimmten technischen Maßnahmen wie der Luftfreihaltung des Systems von der Entnahme bis zur Rückgabe des Wassers reduziert oder vermieden werden.
Jedenfalls wird im Vorfeld eine chemische Analyse des Grundwassers am Standort empfohlen.`,
Metallkorrosion: `Am Standort kann es zur Metallkorrosion in den Brunnen kommen. Dies kann mit bestimmten technischen Maßnahmen, wie einem Wärmetauscher aus rostfreiem Stahl bzw. einem zusätzlichen Trennwärmetauscher reduziert oder vermieden werden. Jedenfalls wird im Vorfeld eine chemische Analyse des Grundwassers am Standort empfohlen.`,
'Keine Daten': `Auf Grund fehlender chemischer Wasseranalysen können keine Aussagen zur Grundwasserchemie getroffen werden.`,
'Kein Risiko durch GW-Chemismus': 'Kein Risiko durch GW-Chemismus',
},
Naturdenkmal: `Am Standort gibt es Naturdenkmäler, die eine Nutzung der Oberflächennahen Geothermie eventuell beschränken können.`,
'Gespannte Grundwasserzone': `Am Standort können gespannte Grundwasserverhältnisse auftreten. Bei der Planung und Durchführung zukünftiger Bohrungen in diesem Bereich muss dies berücksichtigt werden.`,
Gasvorkommen: `Am Standort können oberflächennahe Gasvorkommen nicht ausgeschlossen werden. Bei der Planung und Durchführung zukünftiger Bohrungen in diesem Bereich muss dies berücksichtigt werden.`,
'Mehrere Grundwasserstockwerke': `Am Standort können mehrere Grundwasserstockwerke angetroffen werden.`,
};
const getEinschraenkungen = (attributes: any) => {
let einschraenkungen = [];
// Wasserschutz- und Wasserschongebiete
switch (attributes['EWS_01']) {
case 'Grün':
einschraenkungen.push(
<tr className="w-full" key={einschraenkungen.length}>
<td className={tableDataCSS}>{attributes['Para_01']}</td>
<td className={tableDataCSS}>{getAmpelText('Grün')}</td>
</tr>
);
break;
case 'Gelb':
einschraenkungen.push(
<tr className="w-full" key={einschraenkungen.length}>
<td className={tableDataCSS}>
{attributes['Para_01']}: {attributes['Kat_01']}
</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
case 'Magenta':
einschraenkungen.push(
<tr className="w-full" key={einschraenkungen.length}>
<td className={tableDataCSS}>
{attributes['Para_01']}: {attributes['Kat_01']}
</td>
<td className={tableDataCSS}>{getAmpelText('Magenta')}</td>
</tr>
);
break;
default:
break;
}
// Altlasten
switch (attributes['EWS_02']) {
case 'Gelb':
einschraenkungen_erlaeuterungen[attributes['Para_02']] = (
<>
{einschraenkungenText[attributes['Para_02']]}
<a href={einschraenkungenText[attributes['Para_02'] + '_links']}>
{einschraenkungenText[attributes['Para_02'] + '_links']}
</a>
</>
);
einschraenkungen.push(
<tr key={einschraenkungen.length} className="w-full">
<td className={tableDataCSS}>
{attributes['Para_02']}
<sup>{Object.keys(einschraenkungen_erlaeuterungen).length}</sup>
</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
default:
break;
}
// Artesisch gespannte Brunnen
switch (attributes['EWS_03']) {
case 'Gelb':
einschraenkungen_erlaeuterungen[attributes['Para_03']] = einschraenkungenText[attributes['Para_03']];
einschraenkungen.push(
<tr key={einschraenkungen.length} className="w-full">
<td className={tableDataCSS}>
{attributes['Para_03']}
<sup>{Object.keys(einschraenkungen_erlaeuterungen).length}</sup>
</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
default:
break;
}
// Bergbaugebiete
switch (attributes['EWS_04']) {
case 'Gelb':
einschraenkungen.push(
<tr key={einschraenkungen.length} className="w-full">
<td className={tableDataCSS}>{attributes['Para_04']}</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
default:
break;
}
// Karstzonen
switch (attributes['EWS_05']) {
case 'Gelb':
einschraenkungen_erlaeuterungen[attributes['Para_05']] = einschraenkungenText[attributes['Para_05']];
einschraenkungen.push(
<tr key={einschraenkungen.length} className="w-full">
<td className={tableDataCSS}>{attributes['Para_05']}</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
default:
break;
}
// Naturschutz
switch (attributes['EWS_06']) {
case 'Gelb':
einschraenkungen_erlaeuterungen[attributes['Para_06']] = (
<>
Sind durch ein Vorhaben ein Schutzgebiet (z.B. Nationalpark, Europaschutzgebiet, Landschaftsschutzgebiet,
Naturschutzgebiet, geschützter Landschafsteil), ein Schutzobjekt (Naturdenkmal) oder streng (geschützte) Tier-
und Pflanzenarten betroffen, ist jedenfalls rechtzeitig mit der Magistratsabteilung 22 - Umweltschutz Kontakt
aufzunehmen, um eine allfällige naturschutzbehördliche Bewilligungspflicht abklären zu können. Auf folgenden
Seiten sind sämtliche Informationen zu Schutzgebieten und objekten sowie zu den Artenschutzbestimmungen zu
finden:
<ul className="pt-2">
<li>
<a href="https://www.wien.gv.at/umweltschutz/naturschutz/gebiet/schutzgebiete.html#schutzgebiete">
https://www.wien.gv.at/umweltschutz/naturschutz/gebiet/schutzgebiete.html#schutzgebiete
</a>
</li>
<li>
<a href="https://www.wien.gv.at/umweltschutz/naturschutz/biotop/artenschutz.html">
https://www.wien.gv.at/umweltschutz/naturschutz/biotop/artenschutz.html
</a>
</li>
</ul>
</>
);
einschraenkungen.push(
<tr key={einschraenkungen.length} className="w-full">
<td className={tableDataCSS}>
{attributes['Para_06']}
<sup>{Object.keys(einschraenkungen_erlaeuterungen).length}</sup>:{' '}
{attributes['Kat_06'].replaceAll(',', ', ')}
</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
default:
break;
}
return einschraenkungen;
};
const getHinweise = (attributes: any) => {
let hinweise = [];
if (attributes['Hinweis_01']) {
hinweise.push(
<tr key={hinweise.length} className="w-full">
<td className={tableDataCSS}>{hinweiseText[attributes['Hinweis_01']]}</td>
</tr>
);
}
if (attributes['Hinweis_02']) {
hinweise.push(
<tr key={hinweise.length} className="w-full">
<td className={tableDataCSS}>{hinweiseText[attributes['Hinweis_02']]}</td>
</tr>
);
}
if (attributes['Hinweis_03']) {
hinweise.push(
<tr key={hinweise.length} className="w-full">
<td className={tableDataCSS}>{hinweiseText[attributes['Hinweis_03']]}</td>
</tr>
);
}
return hinweise;
};
export const getAmpelText = (color: string) => {
switch (color) {
case 'Grün':
return 'Nutzung generell möglich';
case 'Gelb':
return 'Genauere Beurteilung notwendig';
case 'Magenta':
return 'Nutzung generell nicht möglich';
default:
return;
}
};
export const TableAmpelkarteEWS = ({ results }: { results: any }) => {
let einschraenkungen: any, hinweise: any;
einschraenkungen_erlaeuterungen = {};
results.forEach((result: any) => {
const attributes = result.feature.attributes;
if (result.layerId === 0) {
einschraenkungen = getEinschraenkungen(attributes);
} else {
hinweise = getHinweise(attributes);
}
});
return (
<>
{einschraenkungen && einschraenkungen.length > 0 ? (
<Collapsible title="Einschränkungen" open={true}>
<table id={'einschraenkungen-table'} className="table-fixed w-full mb-4">
<thead>
<tr>
<td colSpan={2}></td>
</tr>
</thead>
<tbody>{einschraenkungen}</tbody>
<tbody>
{Object.keys(einschraenkungen_erlaeuterungen).length > 0 &&
Object.keys(einschraenkungen_erlaeuterungen).map((key, index) => {
return (
<tr key={key} className="w-full">
<td colSpan={2} className="w-full break-words border-b border-solid border-gray-300 px-2 text-xs">
{index + 1}: {einschraenkungen_erlaeuterungen[key]}
</td>
</tr>
);
})}
</tbody>
</table>
</Collapsible>
) : (
<Collapsible title="Einschränkungen" open={true}>
<table id={'einschraenkungen-table'} className="table-fixed w-full mb-4">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>
<tr className="w-full">
<td className={tableDataCSS}>An diesem Standort sind keine Einschränkungen bekannt.</td>
</tr>
</tbody>
</table>
</Collapsible>
)}
{hinweise && hinweise.length > 0 ? (
<Collapsible title="Hinweise" open={true}>
<table id={'hinweise-table'} className="table-fixed w-full mb-4">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>{hinweise}</tbody>
</table>
</Collapsible>
) : null}
</>
);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -4,7 +4,7 @@
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-start-rgb: 255, 255, 255;
--background-end-rgb: 255, 255, 255;
}
@ -16,12 +16,21 @@
}
}
@import '@arcgis/core/assets/esri/themes/light/main.css';
html,
body {
height: 100%;
width: 100%;
margin: 0px;
padding: 0px;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
}
* {
box-sizing: border-box;
}

View File

@ -0,0 +1,25 @@
.esri-component {
max-height: 80%;
}
.esri-ui-corner .esri-component.esri-layer-list.esri-widget.esri-widget--panel {
}
.esri-component.esri-legend.esri-widget.esri-widget--panel {
}
.esri-ui-top-left.esri-ui-corner {
height: 95%;
}
.esri-layer-list {
background-color: #fff;
}
.esri-search {
width: 300px;
}
.esri-legend__layer-child-table > .esri-legend__layer-caption {
display: none;
}

View File

@ -0,0 +1,180 @@
import { useEffect, useRef, useState } from 'react';
import { useMediaQuery } from 'react-responsive';
import ArcGISMap from '@arcgis/core/Map';
import Extent from '@arcgis/core/geometry/Extent';
import MapView from '@arcgis/core/views/MapView';
import Basemap from '@arcgis/core/Basemap';
import MapImageLayer from '@arcgis/core/layers/MapImageLayer';
import WMTSLayer from '@arcgis/core/layers/WMTSLayer';
import SpatialReference from '@arcgis/core/geometry/SpatialReference';
import Search from '@arcgis/core/widgets/Search';
import ScaleBar from '@arcgis/core/widgets/ScaleBar';
import GraphicsLayer from '@arcgis/core/layers/GraphicsLayer';
import Graphic from '@arcgis/core/Graphic';
import LayerList from '@arcgis/core/widgets/LayerList';
import Zoom from '@arcgis/core/widgets/Zoom';
import Legend from '@arcgis/core/widgets/Legend';
import FeatureLayer from '@arcgis/core/layers/FeatureLayer';
import Polygon from '@arcgis/core/geometry/Polygon';
import esriConfig from '@arcgis/core/config';
import { watch } from '@arcgis/core/core/reactiveUtils';
import SimpleFillSymbol from '@arcgis/core/symbols/SimpleFillSymbol';
import './esri-ui-grundlagenkarte.css';
import { Vienna, Austria } from '@/public/borders-vienna-austria';
import { BASEMAP_AT_URL, AMPEL_GWWP_URL, RESOURCES_GWWP_URL, SRS } from '@/app/config/config';
import getAddress from '@/app/utils/getAddress';
import identifyAllLayers from '@/app/utils/identify';
import takeScreenshot from '@/app/utils/screenshot';
import { useAppDispatch } from '@/redux/hooks';
import PanelGrundlagenkarte from './panel-grundlagenkarte-gwwp';
// set path for local assets
esriConfig.assetsPath = '/assets';
export default function MapComponent() {
const dispatch = useAppDispatch();
const isMobile = useMediaQuery({ maxWidth: 480 });
const mapDiv = useRef<HTMLDivElement | null>(null);
const [address, setAddress] = useState<string[]>([]);
useEffect(() => {
let view: MapView;
let handle: IHandle;
if (mapDiv.current) {
view = new MapView({
container: mapDiv.current,
extent: new Extent({
xmin: -19000,
ymin: 325000,
xmax: 29000,
ymax: 360000,
spatialReference: new SpatialReference({ wkid: SRS }),
}),
popupEnabled: false,
});
let viennaGraphic = new Graphic({
geometry: new Polygon({ rings: [Austria, Vienna], spatialReference: { wkid: SRS } }),
symbol: {
type: 'simple-fill',
color: [209, 213, 219, 0.65],
style: 'solid',
outline: {
color: 'white',
width: 0,
},
} as unknown as SimpleFillSymbol,
});
// graphic layers
let viennaGraphicsLayer = new GraphicsLayer({ title: 'Wien', listMode: 'hide' });
viennaGraphicsLayer.add(viennaGraphic);
const ampelkarte_gwwp = new FeatureLayer({
url: AMPEL_GWWP_URL + '/0',
title: 'Mögliche Einschränkungen',
visible: false,
listMode: 'show',
opacity: 0.5,
});
const resources_gwwp = new MapImageLayer({
title: 'Ressourcen',
url: RESOURCES_GWWP_URL,
visible: false,
listMode: 'show',
opacity: 0.5,
});
// basemap in Viennese coordinate system due to tranformation inaccuracies from MGI to WGS84
// default transformation in ArcGIS API from MGI to WGS84 is 1306
// transformation 1618 is recommended
const basemap_at = new WMTSLayer({
url: BASEMAP_AT_URL,
listMode: 'hide',
});
let basemap = new Basemap({
baseLayers: [basemap_at],
title: 'basemap.at',
id: 'basemap.at',
spatialReference: { wkid: SRS },
});
let arcgisMap = new ArcGISMap({
basemap: basemap,
layers: [resources_gwwp, ampelkarte_gwwp, viennaGraphicsLayer],
});
const layerList = new LayerList({
view,
});
const search = new Search({
view,
popupEnabled: true,
});
const scaleBar = new ScaleBar({
view: view,
unit: 'metric',
});
const zoom = new Zoom({
view,
layout: 'horizontal',
});
const legend = new Legend({
view,
});
// register event handler for mouse clicks
view.on('immediate-click', (event) => {
if (setAddress) {
takeScreenshot(view, event.mapPoint, dispatch, true);
getAddress(event.mapPoint, setAddress);
identifyAllLayers(view, event.mapPoint, dispatch, 'GWWP');
}
});
// add map to view
view.map = arcgisMap;
// add UI components
view.ui.components = [];
if (!isMobile) {
view.ui.add([zoom, search, layerList], 'top-left');
view.ui.add(scaleBar, 'bottom-left');
}
view.when(() => {
handle = watch(
() => view.map?.layers?.map((layer) => layer.visible),
() => {
if (view.map?.layers?.some((layer) => layer.title !== 'Wien' && layer.visible === true)) {
view.ui?.add(legend, 'top-left');
} else {
view.ui?.remove(legend);
}
}
);
});
}
return () => {
handle?.remove();
view?.destroy();
};
}, [dispatch, isMobile]);
return (
<div ref={mapDiv} className="absolute top-16 bottom-0 w-full">
<PanelGrundlagenkarte address={address}></PanelGrundlagenkarte>
</div>
);
}

9
app/gwwp/page.tsx Normal file
View File

@ -0,0 +1,9 @@
'use client';
import dynamic from 'next/dynamic';
const MapComponent = dynamic(() => import('./grundlagenkarte-gwwp'), { ssr: false });
export default function Grundlagenkarte() {
return <MapComponent></MapComponent>;
}

View File

@ -0,0 +1,184 @@
'use client';
import { useEffect, useState, useRef } from 'react';
import { useMediaQuery } from 'react-responsive';
import Image from 'next/image';
import { updateAmpelkarteGWWP } from '@/redux/ampelkarteGWWPSlice';
import { updateResourcesGWWP } from '@/redux/resourcesGWWPSlice';
import { updateScreenshot } from '@/redux/screenshotSlice';
import { useAppSelector, useAppDispatch } from '@/redux/hooks';
import Collapsible from '@/app/components/collapsible';
import Footer from '@/app/components/footer';
import Warning from '@/app/components/warning';
import { TableAmpelkarteGWWP } from './table-ampelkarte-gwwp';
const textTemplates = {
0: [
`Die flächenspezifische Jahresenergie für eine thermische Grundwassernutzung mit ausgeglichener Jahresbilanz, wobei die im Winter zur Heizung entzogene Wärme im Sommer vollständig wieder zurückgegeben wird,
abhängig von der bestehenden Grundwassertemperatur und einer minimalen Rückgabetemperatur von 5 °C und einer maximalen Rückgabetemperatur von 18 °C beträgt rund `,
' kWh/m²/a',
],
1: [
`Die flächenspezifische Jahresenergie für eine thermische Grundwassernutzung im Heiz- und Kühlbetrieb bei Normbetriebsstunden,
abhängig von der bestehenden Grundwassertemperatur und einer minimalen Rückgabetemperatur von 5 °C und einer maximalen Rückgabetemperatur von 18 °C beträgt rund `,
' kWh/m²/a',
],
2: ['Der Grundwasserspiegel ist am Grundstück in einer Tiefe von rund ', ' m zu erwarten.'],
3: ['Das Grundwasser ist am Grundstück rund ', ' m mächtig.'],
4: ['Die hydraulische Leitfähigkeit (kf-Wert) beträgt am Grundstück rund ', ' m/s.'],
5: ['Die maximale Jahrestemperatur des Grundwassers (für das Jahr 2020) liegt bei ', ' °C.'],
6: ['Die mittlere Jahrestemperatur des Grundwassers (für das Jahr 2020) liegt bei ', ' °C.'],
7: ['Die minimale Jahrestemperatur des Grundwassers (für das Jahr 2020) liegt bei ', ' °C.'],
8: [
'Die maximale Pumpleistung eines Brunnenpaars mit 50 m Abstand zwischen Entnahme- und Rückgabebrunnen beträgt rund ',
' l/s.',
],
9: [
'Die maximale Volllast-Leistung eines Brunnenpaars mit 50 m Abstand zwischen Entnahme- und Rückgabebrunnen beträgt rund ',
' kW.',
],
};
export default function Panel({ address }: { address: string[] }) {
const dispatch = useAppDispatch();
const isMobile = useMediaQuery({ maxWidth: 640 });
const [opened, setOpened] = useState<boolean>(true);
const innerRef = useRef<HTMLDivElement | null>(null);
const ampelkarte = useAppSelector((store) => store.ampelkarteGWWP.value);
const resources = useAppSelector((store) => store.resourcesGWWP.value);
const screenshot = useAppSelector((store) => store.screenshot.value);
// initialize query handlers
useEffect(() => {
dispatch(updateAmpelkarteGWWP([]));
dispatch(updateResourcesGWWP([]));
dispatch(updateScreenshot(''));
return () => {
dispatch(updateAmpelkarteGWWP([]));
dispatch(updateResourcesGWWP([]));
dispatch(updateScreenshot(''));
};
}, [dispatch, isMobile]);
// format values
const formatGWWP = (layerId: number, layerName: string, value: string) => {
if (value !== 'NoData') {
if ([5, 6, 7].includes(layerId)) {
value = parseFloat(value).toFixed(1);
} else if (layerId === 4) {
value = parseFloat(value).toFixed(4);
} else {
value = parseFloat(value).toFixed(0);
}
}
if (value === 'NoData') {
return layerName + ': keine Daten';
} else {
return (textTemplates as any)[layerId][0] + value + (textTemplates as any)[layerId][1];
}
};
const handleClick = () => {
setOpened(!opened);
if (innerRef && innerRef.current) {
innerRef.current.classList.toggle('hidden');
}
};
const tableDataCSS = 'w-full break-words border-b border-solid border-gray-300';
return (
<div
className={`absolute top-4 right-4 ${opened && isMobile ? 'bottom-4' : ''} ${
opened && resources && resources.length > 0 ? 'bottom-4' : ''
} w-full md:w-1/3 xl:w-1/4 pl-8 md:pl-0`}
>
<div
onClick={handleClick}
className="bg-gray-700 text-white cursor-pointer p-4 w-full border-0 hover:bg-gray-500 h-12 items-center justify-between text-sm xl:text-base"
>
Abfrageergebnis <span className="float-right">{opened ? '-' : '+'}</span>
</div>
<div
className={`max-h-[calc(100%-3rem)] px-2 xl:px-4 py-4 overflow-y-auto bg-white text-sm ${
opened ? '' : 'hidden'
}`}
ref={innerRef}
>
{screenshot ? <Image src={screenshot} alt="Screenshot" width={1000} height={500}></Image> : null}
{address && address.length > 0 ? (
<table id="address-table" className="table-fixed w-full mt-3 mr-0 mb-3 ml-0">
<tbody>
<tr>
<td>
{address[0]}
<br></br>
{address[1]} {address[3]}
</td>
</tr>
</tbody>
</table>
) : (
<div className="pb-2">
<Warning>Mit einem Klick in die Karte können Sie geothermische Daten abfragen.</Warning>
</div>
)}
{resources && resources.length > 0 ? (
<Collapsible title="Ressourcen" open={true}>
<table id="resources-table" className="table-fixed w-full mb-4">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>
<tr className="w-full">
<th className="pt-4 pb-2 text-center">Ressourcen für vordefinierte Grundwassernutzung</th>
</tr>
{resources.slice(0, 2).map((result: any) => {
return (
<tr className="w-full" key={result.layerId}>
<td className={tableDataCSS}>
{formatGWWP(
result.layerId,
result.layerName,
result.feature.attributes['Classify.Pixel Value']
)}
</td>
</tr>
);
})}
<tr className="w-full">
<th className="pt-4 pb-2 text-center">Standortabhängige Parameter</th>
</tr>
{resources.slice(2, 10).map((result: any) => {
return (
<tr className="w-full" key={result.layerId}>
<td className={tableDataCSS}>
{formatGWWP(
result.layerId,
result.layerName,
result.feature.attributes['Classify.Pixel Value']
)}
</td>
</tr>
);
})}
</tbody>
</table>
</Collapsible>
) : null}
{ampelkarte && ampelkarte.length > 0 ? <TableAmpelkarteGWWP results={ampelkarte}></TableAmpelkarteGWWP> : null}
<Footer></Footer>
</div>
</div>
);
}

View File

@ -0,0 +1,311 @@
import Collapsible from '../components/collapsible';
const hinweiseText: any = {
Grundwasserchemismus: {
'Eisen- und Manganausfällung': `Am Standort kann es zu Eisen- und Manganausfällungen in den Brunnen kommen.
Diese können mit bestimmten technischen Maßnahmen wie der Luftfreihaltung des Systems von der Entnahme bis zur Rückgabe des Wassers reduziert oder vermieden werden.
Jedenfalls wird im Vorfeld eine chemische Analyse des Grundwassers am Standort empfohlen.`,
Metallkorrosion: `Am Standort kann es zur Metallkorrosion in den Brunnen kommen. Dies kann mit bestimmten technischen Maßnahmen, wie einem Wärmetauscher aus rostfreiem Stahl bzw. einem zusätzlichen Trennwärmetauscher reduziert oder vermieden werden. Jedenfalls wird im Vorfeld eine chemische Analyse des Grundwassers am Standort empfohlen.`,
'Keine Daten': `Auf Grund fehlender chemischer Wasseranalysen können keine Aussagen zur Grundwasserchemie getroffen werden.`,
'Kein Risiko durch GW-Chemismus': 'Kein Risiko durch GW-Chemismus',
},
Naturdenkmal: `Am Standort gibt es Naturdenkmäler, die eine Nutzung der Oberflächennahen Geothermie eventuell beschränken können.`,
'Gespannte Grundwasserzone': `Am Standort können gespannte Grundwasserverhältnisse auftreten. Bei der Planung und Durchführung zukünftiger Bohrungen in diesem Bereich muss dies berücksichtigt werden.`,
Gasvorkommen: `Am Standort können oberflächennahe Gasvorkommen nicht ausgeschlossen werden. Bei der Planung und Durchführung zukünftiger Bohrungen in diesem Bereich muss dies berücksichtigt werden.`,
'Mehrere Grundwasserstockwerke': `Am Standort können mehrere Grundwasserstockwerke angetroffen werden.`,
};
const einschraenkungenText: any = {
Naturschutz: `Sind durch ein Vorhaben ein Schutzgebiet (z.B. Nationalpark, Europaschutzgebiet, Landschaftsschutzgebiet, Naturschutzgebiet, geschützter Landschafsteil),
ein Schutzobjekt (Naturdenkmal) oder streng (geschützte) Tier- und Pflanzenarten betroffen, ist jedenfalls rechtzeitig mit der Magistratsabteilung 22 - Umweltschutz Kontakt aufzunehmen,
um eine allfällige naturschutzbehördliche Bewilligungspflicht abklären zu können. Auf folgenden Seiten sind sämtliche Informationen zu Schutzgebieten und objekten sowie zu den Artenschutzbestimmungen zu finden:`,
Naturschutz_links: [
'https://www.wien.gv.at/umweltschutz/naturschutz/gebiet/schutzgebiete.html#schutzgebiete',
'https://www.wien.gv.at/umweltschutz/naturschutz/biotop/artenschutz.html',
],
'Artesisch gespannte Brunnen': `In einem Umkreis von 100 m Radius wurde mit Bohrungen artesisch gespanntes Grundwasser angetroffen. Bei der Planung und Durchführung zukünftiger Bohrungen in diesem Bereich muss dies berücksichtigt werden.`,
Karstzonen: `Am Standort treten verkarstungsfähige Gesteine auf. Bohrungen können daher Hohlräume antreffen.`,
Altlasten: `Es befindet sich eine Altlast am Standort. Weitere Informationen über die Altlasten sind im Altlasten-GIS unter folgendem Link zu finden: `,
Altlasten_links: 'https://secure.umweltbundesamt.at/altlasten/?servicehandler=publicgis',
'Unterirdische Bauwerke': `In einem Umkreis von 2 m befindet sich ein unterirdisches Verkehrsbauwerk (entsprechend Digitalem Verkehrsgraph - GIP). Auf diesen Flächen und im Nahbereich ist der Einsatz von Erdwärmesonden ausgeschlossen.
Es kann jedoch noch weitere unterirdische Bauwerke wie Tiefgaragen, Verbindungsgänge oder Einbauten im restlichen Stadtgebiet geben, die den Einsatz von Erdwärmesonden beschränken können.`,
};
const tableDataCSS = 'w-full break-words border-b border-solid border-gray-300 px-2 text-sm';
let einschraenkungen_erlaeuterungen: any = {};
let hinweise_erlaeuterungen: any = {};
const getEinschraenkungen = (attributes: any) => {
let einschraenkungen = [];
// Wasserschutz- und Wasserschongebiete
switch (attributes['GWP_01']) {
case 'Grün':
einschraenkungen.push(
<tr key={einschraenkungen.length}>
<td className={tableDataCSS}>{attributes['Para_01']}</td>
<td className={tableDataCSS}>{getAmpelText('Grün')}</td>
</tr>
);
break;
case 'Gelb':
einschraenkungen.push(
<tr key={einschraenkungen.length}>
<td className={tableDataCSS}>
{attributes['Para_01']}: {attributes['Kat_01']}
</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
case 'Magenta':
einschraenkungen.push(
<tr key={einschraenkungen.length}>
<td className={tableDataCSS}>
{attributes['Para_01']}: {attributes['Kat_01']}
</td>
<td className={tableDataCSS}>{getAmpelText('Magenta')}</td>
</tr>
);
break;
default:
break;
}
// Altlasten
switch (attributes['GWP_02']) {
case 'Gelb':
einschraenkungen_erlaeuterungen[attributes['Para_02']] = (
<>
{einschraenkungenText[attributes['Para_02']]}
<a href={einschraenkungenText[attributes['Para_02'] + '_links']}>
{einschraenkungenText[attributes['Para_02'] + '_links']}
</a>
</>
);
einschraenkungen.push(
<tr key={einschraenkungen.length}>
<td className={tableDataCSS}>
{attributes['Para_02']}
<sup>{Object.keys(einschraenkungen_erlaeuterungen).length}</sup>
</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
default:
break;
}
// Artesisch gespannte Brunnen
switch (attributes['GWP_03']) {
case 'Gelb':
einschraenkungen_erlaeuterungen[attributes['Para_03']] = einschraenkungenText[attributes['Para_03']];
einschraenkungen.push(
<tr key={einschraenkungen.length}>
<td className={tableDataCSS}>
{attributes['Para_03']}
<sup>{Object.keys(einschraenkungen_erlaeuterungen).length}</sup>
</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
default:
break;
}
// Bergbaugebiete
switch (attributes['GWP_04']) {
case 'Gelb':
einschraenkungen.push(
<tr key={einschraenkungen.length}>
<td className={tableDataCSS}>{attributes['Para_04']}</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
default:
break;
}
// Karstzonen
switch (attributes['GWP_05']) {
case 'Gelb':
einschraenkungen_erlaeuterungen[attributes['Para_05']] = einschraenkungenText[attributes['Para_05']];
einschraenkungen.push(
<tr key={einschraenkungen.length}>
<td className={tableDataCSS}>{attributes['Para_05']}</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
default:
break;
}
// Naturschutz
switch (attributes['GWP_06']) {
case 'Gelb':
einschraenkungen_erlaeuterungen[attributes['Para_06']] = (
<>
Sind durch ein Vorhaben ein Schutzgebiet (z.B. Nationalpark, Europaschutzgebiet, Landschaftsschutzgebiet,
Naturschutzgebiet, geschützter Landschafsteil), ein Schutzobjekt (Naturdenkmal) oder streng (geschützte) Tier-
und Pflanzenarten betroffen, ist jedenfalls rechtzeitig mit der Magistratsabteilung 22 - Umweltschutz Kontakt
aufzunehmen, um eine allfällige naturschutzbehördliche Bewilligungspflicht abklären zu können. Auf folgenden
Seiten sind sämtliche Informationen zu Schutzgebieten und objekten sowie zu den Artenschutzbestimmungen zu
finden:
<ul className="pt-2">
<li>
<a href="https://www.wien.gv.at/umweltschutz/naturschutz/gebiet/schutzgebiete.html#schutzgebiete">
https://www.wien.gv.at/umweltschutz/naturschutz/gebiet/schutzgebiete.html#schutzgebiete
</a>
</li>
<li>
<a href="https://www.wien.gv.at/umweltschutz/naturschutz/biotop/artenschutz.html">
https://www.wien.gv.at/umweltschutz/naturschutz/biotop/artenschutz.html
</a>
</li>
</ul>
</>
);
einschraenkungen.push(
<tr key={einschraenkungen.length} className="w-full">
<td className={tableDataCSS}>
{attributes['Para_06']}
<sup>{Object.keys(einschraenkungen_erlaeuterungen).length}</sup>:{' '}
{attributes['Kat_06'].replaceAll(',', ', ')}
</td>
<td className={tableDataCSS}>{getAmpelText('Gelb')}</td>
</tr>
);
break;
default:
break;
}
return einschraenkungen;
};
const getHinweise = (attributes: any) => {
let hinweise = [];
if (attributes['Hinweis_01']) {
hinweise.push(
<tr key={hinweise.length}>
<td className={tableDataCSS}>{attributes['Hinweis_01']}</td>
<td className={tableDataCSS}>
{attributes['Kat_01']} {attributes['Kat_01'] !== 'Kein Risiko durch GW-Chemismus' && <sup>1</sup>}
</td>
</tr>
);
if (attributes['Kat_01'] !== 'Kein Risiko durch GW-Chemismus') {
hinweise_erlaeuterungen[attributes['Hinweis_01']] = (
<>{hinweiseText.Grundwasserchemismus[attributes['Kat_01']]}</>
);
}
}
return hinweise;
};
export const getAmpelText = (color: string) => {
switch (color) {
case 'Grün':
return 'Nutzung generell möglich';
case 'Gelb':
return 'Genauere Beurteilung notwendig';
case 'Magenta':
return 'Nutzung generell nicht möglich';
default:
return;
}
};
export const TableAmpelkarteGWWP = ({ results }: { results: any }) => {
let einschraenkungen: any[] = [];
let hinweise: any[] = [];
einschraenkungen_erlaeuterungen = {};
results.forEach((result: any) => {
const attributes = result.feature.attributes;
if (result.layerId === 0) {
einschraenkungen = getEinschraenkungen(attributes);
} else {
hinweise = getHinweise(attributes);
}
});
return (
<>
{einschraenkungen.length > 0 ? (
<Collapsible title="Einschränkungen" open={true}>
<table id={'einschraenkungen-table'} className="table-fixed w-full mb-4">
<thead>
<tr>
<td colSpan={2}></td>
</tr>
</thead>
<tbody>{einschraenkungen}</tbody>
<tbody>
{Object.keys(einschraenkungen_erlaeuterungen).length > 0
? Object.keys(einschraenkungen_erlaeuterungen).map((key, index) => {
return (
<tr key={key} className="w-full">
<td
colSpan={2}
className="w-full break-words border-b border-solid border-gray-300 px-2 text-xs"
>
{index + 1}: {einschraenkungen_erlaeuterungen[key]}
</td>
</tr>
);
})
: null}
</tbody>
</table>
</Collapsible>
) : (
<Collapsible title="Einschränkungen" open={true}>
<table id={'einschraenkungen-table'} className="table-fixed w-full mb-4">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>
<tr className="w-full">
<td className={tableDataCSS}>An diesem Standort sind keine Einschränkungen bekannt.</td>
</tr>
</tbody>
</table>
</Collapsible>
)}
{hinweise.length > 0 ? (
<Collapsible title="Hinweise" open={true}>
<table id={'hinweise-table'} className="table-fixed w-full mb-4">
<thead>
<tr>
<td></td>
</tr>
</thead>
<tbody>{hinweise}</tbody>
<tbody>
{Object.keys(hinweise_erlaeuterungen).length > 0 &&
Object.keys(hinweise_erlaeuterungen).map((key, index) => {
return (
<tr key={key}>
<td colSpan={2} className="w-full break-words border-b border-solid border-gray-300 px-2 text-xs">
{index + 1}: {hinweise_erlaeuterungen[key]}
</td>
</tr>
);
})}
</tbody>
</table>
</Collapsible>
) : null}
</>
);
};

View File

@ -1,22 +1,45 @@
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css';
const inter = Inter({ subsets: ['latin'] })
import type { Metadata } from 'next';
import Image from 'next/image';
import { Inter } from 'next/font/google';
import Navigation from './navigation';
import MobileNavigation from './mobile-navigation';
import ReduxProvider from '@/redux/provider';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
title: 'Geothermie Atlas',
description: 'Generated by GeoSphere Austria',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body className={inter.className}>
<div className="h-16 flex flex-row items-center border-b border-solid border-slate-300 px-8 xl:px-32 max-w-full">
<Image
src="/geosphere-austria-logo.png"
alt="GeoSphere Austria Logo"
className="w-16 xl:w-28 object-contain"
width={296}
height={92}
></Image>
<Image
src="/stadt-wien-logo.png"
alt="Stadt Wien Logo"
className="w-16 xl:w-28 object-contain pl-4 pl-4 xl:pl-10"
width={170}
height={76}
></Image>
<span className="pl-5 xl:pl-10 text-xs xl:text-base">Geothermie Atlas</span>
<Navigation></Navigation>
</div>
<MobileNavigation></MobileNavigation>
<ReduxProvider>{children}</ReduxProvider>
</body>
</html>
)
);
}

89
app/mobile-navigation.tsx Normal file
View File

@ -0,0 +1,89 @@
'use client';
import { useRef } from 'react';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
const linkCSS = 'hover:underline hover:decoration-red-700 hover:underline-offset-4';
export default function Navigation() {
const pathname = usePathname();
const menuDiv = useRef<HTMLDivElement | null>(null);
const decoration = 'underline decoration-red-700 underline-offset-4';
const handleClick = () => {
if (menuDiv.current) {
menuDiv.current.classList.toggle('hidden');
}
};
return (
<div className="md:hidden ">
<button
className="absolute top-5 right-5 inline-flex hover:text-red-700 lg:hidden ml-auto outline-none"
onClick={handleClick}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<div className="hidden absolute top-0 right-0 bottom-0 bg-white w-5/6 h-full z-50" ref={menuDiv}>
<button
className="absolute top-5 right-5 inline-flex hover:text-red-700 lg:hidden ml-auto outline-none"
onClick={handleClick}
>
<svg
className="w-6 h-6"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 21.32L21 3.32001" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
<path d="M3 3.32001L21 21.32" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<div className="absolute top-20 pl-8 ">
<ul className="flex flex-col justify-between gap-y-4 text-sm">
<li>
<Link href="/" className={pathname === '/' ? decoration : linkCSS}>
Home
</Link>
</li>
<li>
<Link href="/ews/grundlagenkarte" className={linkCSS}>
Grundlagenkarte Erdwärme
</Link>
</li>
<li>
<Link href="/ews/potenzialberechnung" className={linkCSS}>
Potenzialberechnung Erdwärme
</Link>
</li>
<li>
<Link href="/gwwp" className={pathname === '/gwwp' ? decoration : linkCSS}>
Grundwasserwärmepumpen
</Link>
</li>
<li>
<Link href="/daten" className={pathname === '/daten' ? decoration : linkCSS}>
Daten
</Link>
</li>
<li>
<Link href="/about" className={pathname === '/about' ? decoration : linkCSS}>
About
</Link>
</li>
</ul>
</div>
</div>
</div>
);
}

57
app/navigation.tsx Normal file
View File

@ -0,0 +1,57 @@
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
const linkCSS = 'hover:underline hover:decoration-red-700 hover:underline-offset-4 text-xs xl:text-base';
const Tooltip = ({ pathname }: { pathname: string }) => {
const css = pathname.startsWith('/ews')
? 'group relative inline-block hover:cursor-pointer duration-300 underline decoration-red-700 underline-offset-4 text-xs xl:text-base'
: 'group relative inline-block hover:cursor-pointer duration-300 text-xs xl:text-base';
return (
<div>
<div className={css}>
Erdwärmesonden
<div className="absolute hidden group-hover:flex -top-3 text-sm w-fit py-px bg-white z-50">
<ul>
<li>
<Link href="/ews/grundlagenkarte" className={linkCSS}>
Grundlagenkarte
</Link>
</li>
<li>
<Link href="/ews/potenzialberechnung" className={linkCSS}>
Potenzialberechnung
</Link>
</li>
</ul>
</div>
</div>
</div>
);
};
export default function Navigation() {
const pathname = usePathname();
const decoration = 'underline decoration-red-700 underline-offset-4 text-xs xl:text-base';
return (
<div className="hidden md:block lg:pl-32 md:flex md:justify-start gap-x-8 xl:gap-x-10 text-xs xl:text-base">
<Link href="/" className={pathname === '/' ? decoration : linkCSS}>
Home
</Link>
<Tooltip pathname={pathname}></Tooltip>
<Link href="/gwwp" className={pathname === '/gwwp' ? decoration : linkCSS}>
Grundwasserwärmepumpen
</Link>
<Link href="/daten" className={pathname === '/daten' ? decoration : linkCSS}>
Daten
</Link>
<Link href="/about" className={pathname === '/about' ? decoration : linkCSS}>
About
</Link>
</div>
);
}

View File

@ -1,113 +1,80 @@
import Image from 'next/image'
import Link from 'next/link';
import Image from 'next/image';
const oldLink = (
<Link href="/ews" className="group-hover:">
<Image src="/EWS.jpg" width={575} height={663} alt="Erdwärmesonden" className="mx-auto"></Image>
<p className="max-w-screen-md mt-4">
Nutzen Sie die Wärme des Untergrunds mit Erdwärmesonden. Dieses System nutzt vertikale Bohrungen, in denen eine
Wärmeträgerflüssigkeit zirkuliert und über die der Wärmeaustausch stattfindet.
</p>
</Link>
);
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">app/page.tsx</code>
<div className="absolute bottom-0 top-16 left-0 right-0 overflow-y-auto text-sm xl:text-base mb-4">
<div className="max-w-screen-2xl mx-auto mt-8 mb-4 px-8 xl:px-24">
<h1 className="text-red-700 text-base md:text-xl xl:text-2xl mb-2 font-semibold">
Willkommen beim Geothermie-Atlas!
</h1>
<p className="whitespace-normal wordbreak-normal">
Hier erhalten Sie maßgeschneiderte Informationen, um fundierte Entscheidungen über oberflächennahe
Geothermie-Systeme an gewählten Standorten treffen zu können. Ob Sie HausbesitzerIn, ProjektentwicklerIn,
InvestorIn sind, oder sich allgemein für erneuerbare Energiequellen interessieren, unsere Karten und die
Grundstücksabfrage bieten Ihnen wertvolle Einblicke in das Energiepotential Ihres Grundstücks. Momentan ist
der Geothermie-Atlas für Wien verfügbar, an einer österreichweiten Ergänzung wird gearbeitet. Nutzen Sie die
Kraft der Geothermie! Klicken Sie unten, um zu den Karten und der Grundstücksabfrage zu gelangen.
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
</div>
<div className="absolute px-8 flex justify-center flex-col md:flex-row w-full gap-x-2 gap-y-2">
<div className="px-2 md:px-5 py-4 max-w-lg xl:max-w-2xl border border-gray-300">
<div className="flex justify-between">
<Link
href="/ews/grundlagenkarte"
className="shrink group border border-transparent px-2 xl:px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
<h2 className={`mb-3 text-sm md:text-base`}>Zur Grundlagenkarte</h2>
<p className={`m-0 max-w-[30ch] text-xs xl:text-sm opacity-50`}>
Grundlegende Abfragen zum Thema Erdwärmesonden
</p>
</Link>
<Link
href="/ews/potenzialberechnung"
className="shrink group border border-transparent px-2 xl:px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100"
>
<h2 className={`mb-3 text-sm md:text-base`}>Zur Potenzialberechnung</h2>
<p className={`m-0 max-w-[30ch] text-xs xl:text-sm opacity-50`}>
Grundstücksbezogene Potenzialberechnung
</p>
</Link>
</div>
<Image src="/EWS.jpg" width={575} height={663} alt="Erdwärmesonden" className="mx-auto"></Image>
<p className="max-w-screen-md mt-4 mb-4 xl:px-8 text-xs xl:text-sm">
Nutzen Sie die Wärme des Untergrunds mit Erdwärmesonden. Dieses System nutzt vertikale Bohrungen, in denen
eine Wärmeträgerflüssigkeit zirkuliert und über die der Wärmeaustausch stattfindet.
</p>
</div>
<div className="px-2 md:px-5 py-4 max-w-lg xl:max-w-2xl border border-gray-300">
<div className="flex justify-start">
<Link
href="/gwwp"
className="group border border-transparent px-2 xl:px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100"
>
<h2 className={`mb-3 text-sm md:text-base`}>Zur Grundlagenkarte</h2>
<p className={`m-0 max-w-[30ch] text-xs xl:text-sm opacity-50`}>
Grundlegende Abfragen zum Thema Grundwasserwärmepumpen
</p>
</Link>
</div>
<Image src="/GWWP.jpg" width={575} height={663} alt="Grundwasserwärmepumpen" className="mx-auto"></Image>
<p className="max-w-screen-md mt-4 mb-4 xl:px-8 text-xs xl:text-sm">
Ein weiteres effizientes System ist die thermische Grundwassernutzung. Hier wird nach der Entnahme von einem
Brunnen die Wärme des Grundwassers an das Gebäude übertragen und anschließend das Wasser über einen
Schluckbrunnen dem Grundwasserkörper zurückgegeben.
</p>
</div>
</div>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Docs{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Learn{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Templates{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Explore the Next.js 13 playground.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Deploy{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
)
);
}

17
app/utils/getAddress.js Normal file
View File

@ -0,0 +1,17 @@
import * as locator from '@arcgis/core/rest/locator';
// reverse-geocode address for a given point
export default function getAddress(mapPoint, setAddress) {
const serviceUrl = 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer';
const params = {
location: mapPoint,
};
locator.locationToAddress(serviceUrl, params).then(
function (response) {
setAddress(response.address.split(','));
},
function () {
setAddress([]);
}
);
}

85
app/utils/identify.js Normal file
View File

@ -0,0 +1,85 @@
import { identify } from '@arcgis/core/rest/identify';
import IdentifyParameters from '@arcgis/core/rest/support/IdentifyParameters';
import { updateAmpelkarteEWS } from '@/redux/ampelkarteEWSSlice';
import { updateAmpelkarteGWWP } from '@/redux/ampelkarteGWWPSlice';
import { updateResourcesEWS } from '@/redux/resourcesEWSSlice';
import { updateResourcesGWWP } from '@/redux/resourcesGWWPSlice';
import { RESOURCES_EWS_URL, AMPEL_EWS_URL, RESOURCES_GWWP_URL, AMPEL_GWWP_URL } from '../config/config';
// query layers
export default function identifyAllLayers(view, mapPoint, dispatch, theme) {
// define query parameters
const params = new IdentifyParameters();
params.geometry = mapPoint;
params.tolerance = 0;
params.layerOption = 'all';
params.width = view.width;
params.height = view.height;
params.mapExtent = view.extent;
if (theme === 'EWS') {
identify(RESOURCES_EWS_URL, params)
.then((res) => {
const results = res.results.map((result) => {
return {
layerId: result.layerId,
layerName: result.layerName,
feature: { attributes: result.feature.attributes },
};
});
dispatch(updateResourcesEWS(results));
})
.catch(() => {
dispatch(updateResourcesEWS([]));
});
identify(AMPEL_EWS_URL, params)
.then((res) => {
const results = res.results.map((result) => {
return {
layerId: result.layerId,
layerName: result.layerName,
feature: { attributes: result.feature.attributes },
};
});
dispatch(updateAmpelkarteEWS(results));
})
.catch(() => {
dispatch(updateAmpelkarteEWS([]));
});
}
if (theme === 'GWWP') {
identify(RESOURCES_GWWP_URL, params)
.then((res) => {
const results = res.results.map((result) => {
return {
layerId: result.layerId,
layerName: result.layerName,
feature: { attributes: result.feature.attributes },
};
});
dispatch(updateResourcesGWWP(results));
})
.catch(() => {
dispatch(updateResourcesGWWP([]));
});
identify(AMPEL_GWWP_URL, params)
.then((res) => {
const results = res.results.map((result) => {
return {
layerId: result.layerId,
layerName: result.layerName,
feature: { attributes: result.feature.attributes },
};
});
dispatch(updateAmpelkarteGWWP(results));
})
.catch(() => {
dispatch(updateAmpelkarteGWWP([]));
});
}
}

420
app/utils/print.js Normal file
View File

@ -0,0 +1,420 @@
import jsPDF from 'jspdf';
import 'jspdf-autotable';
export default function print(
einschraenkungen,
hinweise,
computationResult,
screenshot,
image_bal,
image_userdefined,
cadastralData,
warnings = false,
image_borefield,
calculationMode,
theme,
resources
) {
// space between tables
let spaceBetween = 5;
// create new pdf document object
const doc = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4',
});
// add date to top right corner
let today = new Date().toLocaleDateString();
doc.setFontSize(8);
doc.text('erstellt am ' + today, 190, 10, { align: 'right' });
// add heading
doc.setFontSize(14);
doc.text('Standortbasierter Bericht', 105, 20, {
align: 'center',
});
// screenshot of the map
// doc.addImage(screenshot, 'PNG', 20, 30, 170, 85);
// cadastral data
if (cadastralData) {
doc.autoTable({
html: '#cadastral-data-table',
rowPageBreak: 'avoid',
startY: 30,
styles: { halign: 'center' },
columnStyles: { 0: { fillColor: [255, 255, 255] } },
});
}
// address table
doc.autoTable({
html: '#address-table',
rowPageBreak: 'avoid',
startY: doc.lastAutoTable.finalY ? doc.lastAutoTable.finalY : 30,
styles: { halign: 'center' },
columnStyles: { 0: { fillColor: [255, 255, 255] } },
});
// legend for parcel boundary lines
// if (computationResult && theme === 'EWS') {
// doc.autoTable({
// startY: doc.lastAutoTable.finalY,
// head: [],
// body: [
// [' ', 'Grundstücksgrenze'],
// [' ', '2,5-Meter-Abstand zur Grundstückgsrenze'],
// ],
// willDrawCell: () => {
// doc.setFillColor(255, 255, 255);
// },
// didDrawCell: function (data) {
// let rowCenterY = data.row.height / 2;
// doc.setLineWidth(0.5);
// if (computationResult && data.row.index === 0 && data.column.index === 0) {
// doc.setDrawColor('blue');
// doc.line(data.cursor.x + 5, data.cursor.y + rowCenterY, data.cursor.x + 40, data.cursor.y + rowCenterY);
// }
// if (computationResult && theme === 'EWS' && data.row.index === 1 && data.column.index === 0) {
// doc.setDrawColor('#00890c');
// doc.line(data.cursor.x + 5, data.cursor.y + rowCenterY, data.cursor.x + 40, data.cursor.y + rowCenterY);
// }
// },
// });
// }
// warnings table
if (computationResult && warnings) {
doc.autoTable({
html: '#warnings-table',
rowPageBreak: 'avoid',
startY: doc.lastAutoTable.finalY,
willDrawCell: function (data) {
if (data.section === 'body' && data.cell.text !== '') {
doc.setFillColor(255, 251, 214);
doc.setTextColor(113, 81, 0);
} else {
doc.setFillColor(255, 255, 255);
}
},
});
}
// resources table
if (resources) {
doc.autoTable({
html: '#resources-table',
rowPageBreak: 'avoid',
showHead: 'firstPage',
startY: doc.lastAutoTable.finalY + spaceBetween,
columnStyles: {
0: {
lineWidth: { bottom: 0.1 },
lineColor: '#d1d1d1',
fillColor: [255, 255, 255],
},
},
willDrawCell: function (data) {
if (data.section === 'head') {
doc.setFillColor(5, 46, 55);
data.cell.text = 'Ressourcen';
}
if (
data.cell.text.length > 0 &&
(data.cell.text[0].startsWith('Ressourcen') || data.cell.text[0].startsWith('Standortabhängige'))
) {
data.cell.styles.halign = 'center';
}
},
});
}
// restrictions table
if (einschraenkungen) {
// start at second page
doc.addPage();
doc.autoTable({
html: '#einschraenkungen-table',
rowPageBreak: 'avoid',
showHead: 'firstPage',
startY: 20,
columnStyles: {
0: {
lineWidth: { bottom: 0.1 },
lineColor: '#d1d1d1',
fillColor: [255, 255, 255],
},
1: {
lineWidth: { bottom: 0.1 },
lineColor: '#d1d1d1',
fillColor: [255, 255, 255],
},
},
willDrawCell: (data) => {
if (data.section === 'head') {
doc.setFillColor(5, 46, 55);
data.cell.text = 'Einschränkungen';
}
},
});
}
// hints table
if (hinweise) {
doc.autoTable({
html: '#hinweise-table',
rowPageBreak: 'avoid',
showHead: 'firstPage',
startY: einschraenkungen ? doc.lastAutoTable.finalY + spaceBetween : 20,
columnStyles: {
0: {
lineWidth: { bottom: 0.1 },
lineColor: '#d1d1d1',
fillColor: [255, 255, 255],
},
1: {
lineWidth: { bottom: 0.1 },
lineColor: '#d1d1d1',
fillColor: [255, 255, 255],
},
},
willDrawCell: function (data) {
if (data.section === 'head') {
doc.setFillColor(5, 46, 55);
data.cell.text = 'Hinweise';
}
},
});
}
// calculations input table
if (computationResult) {
if (einschraenkungen || hinweise) {
doc.addPage();
}
doc.autoTable({
html: '#calculations-input-table',
rowPageBreak: 'avoid',
showHead: 'firstPage',
startY: hinweise ? 20 : doc.lastAutoTable.finalY + spaceBetween,
columnStyles: {
0: {
lineWidth: { bottom: 0.1 },
lineColor: '#d1d1d1',
fillColor: [255, 255, 255],
},
},
willDrawCell: function (data) {
if (data.section === 'head') {
doc.setFillColor(5, 46, 55);
data.cell.text[0] = 'Berechnungsergebnisse';
}
if (
data.cell.text[0] === 'Berechnungsvorgaben' ||
data.cell.text[0] === 'Benutzereingabe' ||
data.cell.text[0] === 'Standortabhängige Parameter'
) {
data.cell.styles.halign = 'center';
}
},
});
}
// borefield map
if (computationResult && image_borefield) {
const imgProps = doc.getImageProperties(image_borefield.current);
const width = doc.internal.pageSize.getWidth() - 100;
const totalHeight = doc.internal.pageSize.getHeight();
let height = (imgProps.height * width) / imgProps.width;
if (height > totalHeight - doc.lastAutoTable.finalY - 10) {
doc.addPage();
doc.addImage(image_borefield.current, 'PNG', 50, 20, width, height);
} else {
doc.addImage(image_borefield.current, 'PNG', 50, doc.lastAutoTable.finalY + 5, width, height);
}
}
// start new page if theme is EWS
// user input table is longer than for GWWP
if (computationResult && theme === 'EWS') {
doc.addPage();
}
if (computationResult) {
doc.autoTable({
html: '#calculations-output-table',
rowPageBreak: 'avoid',
showHead: 'firstPage',
startY: theme === 'EWS' ? 20 : doc.lastAutoTable.finalY + spaceBetween,
columnStyles: {
0: {
lineWidth: { bottom: 0.1 },
lineColor: '#d1d1d1',
fillColor: [255, 255, 255],
},
},
willDrawCell: (data) => {
if (data.cell.text[0] === 'Benutzerdefinierte Vorgaben') {
data.cell.styles.halign = 'center';
}
if (
data.cell.text[0].startsWith('Berechnungsergebnisse') ||
data.cell.text[0].startsWith('Heizbetrieb') ||
data.cell.text[0].startsWith('Kühlbetrieb')
) {
doc.setFillColor(255, 255, 255);
data.cell.styles.halign = 'center';
}
},
});
}
// plot graph for user defined input
let height = 0;
if (computationResult && image_userdefined) {
const imgProps = doc.getImageProperties(image_userdefined.current);
const width = doc.internal.pageSize.getWidth() - 60;
const totalHeight = doc.internal.pageSize.getHeight();
height = (imgProps.height * width) / imgProps.width;
if (height > totalHeight - doc.lastAutoTable.finalY - 10) {
doc.addPage();
doc.addImage(image_userdefined.current, 'PNG', 30, 20, width, height);
} else {
doc.addImage(image_userdefined.current, 'PNG', 30, doc.lastAutoTable.finalY + 5, width, height);
}
}
// calculations output for automatic input
if (computationResult && image_bal && image_bal.current) {
doc.addPage();
doc.autoTable({
html: '#calculations-bal-output-table',
rowPageBreak: 'avoid',
showHead: 'firstPage',
startY: 20,
columnStyles: {
0: {
lineWidth: { bottom: 0.1 },
lineColor: '#d1d1d1',
fillColor: [255, 255, 255],
},
},
willDrawCell: (data) => {
if (data.section === 'head') {
doc.setFillColor(5, 46, 55);
data.cell.text[0] = 'Berechnungsergebnisse für den saisonalen Speicherbetrieb';
}
if (
data.cell.text[0] === 'Berechnungsergebnisse' ||
data.cell.text[0].startsWith('Heizbetrieb') ||
data.cell.text[0].startsWith('Kühlbetrieb')
) {
data.cell.styles.halign = 'center';
}
},
});
}
// plot graph for automatic input
if (computationResult && image_bal && image_bal.current) {
const imgProps = doc.getImageProperties(image_bal.current);
const width = doc.internal.pageSize.getWidth() - 60;
const totalHeight = doc.internal.pageSize.getHeight();
height = (imgProps.height * width) / imgProps.width;
if (height > totalHeight - doc.lastAutoTable.finalY - 10) {
doc.addPage();
doc.addImage(image_bal.current, 'PNG', 30, 20, width, height);
} else {
doc.addImage(image_bal.current, 'PNG', 30, doc.lastAutoTable.finalY + 5, width, height);
}
}
// show glossary
doc.addPage();
if (computationResult && theme === 'EWS') {
doc.autoTable({
startY: 20,
head: [
[
{
content: 'Glossar',
colSpan: 2,
styles: { fillColor: [5, 46, 55] },
},
],
],
body: [
['COP', 'Leistungszahl der Wärmepumpe im Heizbetrieb (Coefficient of Performance)'],
['JAZ', 'Jahresarbeitszahl oder saisonale Leistungszahl der Wärmepumpe im Heizbetrieb'],
['EER', 'Leistungszahl der Wärmepumpe im Kühlbetrieb (Energy Efficiency Rating)'],
['SEER', 'Saisonale Leistungszahl der Wärmepumpe im Kühlbetrieb (Seasonal Energy Efficiency Rating)'],
],
columnStyles: {
0: {
lineWidth: { bottom: 0.1 },
lineColor: '#d1d1d1',
fillColor: [255, 255, 255],
},
1: {
lineWidth: { bottom: 0.1 },
lineColor: '#d1d1d1',
fillColor: [255, 255, 255],
},
},
});
}
// disclaimer
doc.autoTable({
html: '#disclaimer',
rowPageBreak: 'avoid',
startY: computationResult && theme === 'EWS' ? doc.lastAutoTable.finalY + spaceBetween : 20,
columnStyles: { 0: { fillColor: [255, 255, 255] } },
willDrawCell: function (data) {
if (data.section === 'head') {
doc.setFillColor(5, 46, 55);
data.cell.text = 'Haftungsausschluss';
}
},
});
// contact
doc.autoTable({
html: '#contact',
rowPageBreak: 'avoid',
startY: doc.lastAutoTable.finalY + spaceBetween,
columnStyles: { 0: { fillColor: [255, 255, 255] } },
willDrawCell: (data) => {
if (data.section === 'head') {
doc.setFillColor(5, 46, 55);
data.cell.text = 'Kontakt';
}
},
});
// print page numbers and number of total pages
const pageCount = doc.internal.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
// go to page i
doc.setPage(i);
// set font size
doc.setFontSize(8);
// print text
doc.text('Seite ' + i, 190, 283, {
align: 'right',
});
}
doc.save('Bericht.pdf');
}

View File

@ -0,0 +1,81 @@
import esriRequest from '@arcgis/core/request';
import * as geometryEngine from '@arcgis/core/geometry/geometryEngine';
import Polygon from '@arcgis/core/geometry/Polygon';
import Graphic from '@arcgis/core/Graphic';
import { updateCadastralData } from '@/redux/cadastreSlice';
import { calculateGrid } from '../ews/potenzialberechnung/gridcomputer';
import { BEV_KATASTER_URL } from '@/app/config/config';
export default function queryCadastre(
view,
polygonGraphicsLayer,
mapPoint,
dispatch,
setPolygon,
setPoints,
gridSpacing = 10
) {
const { x, y } = view.toScreen(mapPoint);
let url =
BEV_KATASTER_URL +
'?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&LAYERS=DKM_GST&QUERY_LAYERS=DKM_GST&CRS=EPSG:31256&INFO_FORMAT=application/json';
const { xmin, ymin, xmax, ymax } = view.extent;
const width = view.width;
const height = view.height;
url +=
'&BBOX=' +
ymin +
',' +
xmin +
',' +
ymax +
',' +
xmax +
'&WIDTH=' +
width +
'&HEIGHT=' +
height +
'&I=' +
Math.round(x) +
'&J=' +
Math.round(y);
esriRequest(url, { responseType: 'json' }).then((response) => {
if (response.data && response.data.features && response.data.features.length > 0) {
const feature = response.data.features[0];
let KG = feature.properties.KG;
let GNR = feature.properties.GNR;
let polygon = new Polygon({
rings: feature.geometry.coordinates,
spatialReference: view.spatialReference,
});
const polygonSymbol = {
type: 'simple-fill',
color: [51, 51, 150, 0],
style: 'solid',
outline: {
color: 'blue',
width: '2px',
},
};
const polygonGraphic = new Graphic({
geometry: polygon,
symbol: polygonSymbol,
});
polygonGraphicsLayer.add(polygonGraphic);
let FF = geometryEngine.planarArea(polygon, 'square-meters');
calculateGrid(polygon, gridSpacing, setPoints);
setPolygon(polygon);
dispatch(updateCadastralData({ KG, GNR, FF }));
}
});
}

59
app/utils/screenshot.js Normal file
View File

@ -0,0 +1,59 @@
import { updateScreenshot } from '@/redux/screenshotSlice';
// take screenshot for info panel
export default function takeScreenshot(view, mapPoint, dispatch, withMarker = false) {
const screenPoint = view.toScreen(mapPoint);
const width = 1000;
const height = 500;
if (withMarker) {
view
.takeScreenshot({
area: {
x: screenPoint.x - width / 2,
y: screenPoint.y - height / 2,
width: width,
height: height,
},
})
.then((screenshot) => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
const img = new Image();
img.width = width;
img.height = height;
img.src = screenshot.dataUrl;
img.onload = () => {
context.drawImage(img, 0, 0);
context.strokeStyle = '#4090D0';
context.lineWidth = 10;
context.beginPath();
context.moveTo(width / 2, height / 2);
context.lineTo(width / 2 + 10, height / 2 - 40);
context.lineTo(width / 2 - 10, height / 2 - 40);
context.closePath();
context.stroke();
dispatch(updateScreenshot(canvas.toDataURL()));
};
});
} else {
view
.takeScreenshot({
area: {
x: screenPoint.x - width / 2,
y: screenPoint.y - height / 2,
width: width,
height: height,
},
})
.then((screenshot) => {
dispatch(updateScreenshot(screenshot.dataUrl));
});
}
}

View File

@ -1,4 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
const nextConfig = {
basePath: '',
};
module.exports = nextConfig
module.exports = nextConfig;

1156
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,25 +3,34 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"dev": "npm run copy && next dev",
"build": "npm run copy && next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"copy": "ncp ./node_modules/@arcgis/core/assets ./public/assets"
},
"dependencies": {
"@arcgis/core": "^4.27.6",
"@reduxjs/toolkit": "^1.9.7",
"jspdf": "^2.5.1",
"jspdf-autotable": "^3.7.0",
"next": "13.5.4",
"python-shell": "^5.0.0",
"react": "^18",
"react-dom": "^18",
"next": "13.5.4"
"react-redux": "^8.1.3",
"react-responsive": "^9.0.2"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "13.5.4",
"ncp": "^2.0.0",
"postcss": "^8",
"tailwindcss": "^3",
"eslint": "^8",
"eslint-config-next": "13.5.4"
"typescript": "^5"
}
}

BIN
public/EWS.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

BIN
public/GWWP.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 KiB

View File

@ -0,0 +1,4 @@
{
"expand": "Expand",
"collapse": "Collapse"
}

View File

@ -0,0 +1,4 @@
{
"expand": "توسيع",
"collapse": "طي"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Разгъване",
"collapse": "Сгъване"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Proširi",
"collapse": "Sažmi"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Amplia",
"collapse": "Redueix"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Rozbalit",
"collapse": "Sbalit"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Udvid",
"collapse": "Skjul"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Einblenden",
"collapse": "Ausblenden"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Ανάπτυξη",
"collapse": "Σύμπτυξη"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Expand",
"collapse": "Collapse"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Expandir",
"collapse": "Contraer"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Laienda",
"collapse": "Ahenda"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Laajenna",
"collapse": "Kutista"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Développer",
"collapse": "Réduire"
}

View File

@ -0,0 +1,4 @@
{
"expand": "הרחב",
"collapse": "צמצם"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Proširi",
"collapse": "Sažmi"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Kibontás",
"collapse": "Összecsukás"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Bentang",
"collapse": "Tutup"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Espandi",
"collapse": "Comprimi"
}

View File

@ -0,0 +1,4 @@
{
"expand": "展開",
"collapse": "折りたたむ"
}

View File

@ -0,0 +1,4 @@
{
"expand": "확장",
"collapse": "축소"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Išskleisti",
"collapse": "Suskleisti"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Izvērst",
"collapse": "Sakļaut"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Uitklappen",
"collapse": "Inklappen"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Utvid",
"collapse": "Skjul"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Rozwiń",
"collapse": "Zwiń"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Expandir",
"collapse": "Recolher"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Expandir",
"collapse": "Recolher"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Extindere",
"collapse": "Restrângere"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Развернуть",
"collapse": "Свернуть"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Rozbaliť",
"collapse": "Zbaliť"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Razširi",
"collapse": "Strni"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Proširi",
"collapse": "Skupi"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Expandera",
"collapse": "Dölj"
}

View File

@ -0,0 +1,4 @@
{
"expand": "ขยาย",
"collapse": "ย่อลงมา"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Genişlet",
"collapse": "Daralt"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Розширити",
"collapse": "Згорнути"
}

View File

@ -0,0 +1,4 @@
{
"expand": "Mở rộng",
"collapse": "Thu gọn"
}

View File

@ -0,0 +1,4 @@
{
"expand": "展开",
"collapse": "折叠"
}

View File

@ -0,0 +1,4 @@
{
"expand": "展開",
"collapse": "摺疊"
}

View File

@ -0,0 +1,4 @@
{
"expand": "展開",
"collapse": "摺疊"
}

View File

@ -0,0 +1,3 @@
{
"more": "More"
}

View File

@ -0,0 +1,3 @@
{
"more": "المزيد"
}

View File

@ -0,0 +1,3 @@
{
"more": "Още"
}

View File

@ -0,0 +1,3 @@
{
"more": "Više"
}

View File

@ -0,0 +1,3 @@
{
"more": "Més"
}

View File

@ -0,0 +1,3 @@
{
"more": "Více"
}

View File

@ -0,0 +1,3 @@
{
"more": "Mere"
}

View File

@ -0,0 +1,3 @@
{
"more": "Mehr"
}

View File

@ -0,0 +1,3 @@
{
"more": "Περισσότερα"
}

View File

@ -0,0 +1,3 @@
{
"more": "More"
}

View File

@ -0,0 +1,3 @@
{
"more": "Más"
}

View File

@ -0,0 +1,3 @@
{
"more": "Rohkem"
}

View File

@ -0,0 +1,3 @@
{
"more": "Enemmän"
}

View File

@ -0,0 +1,3 @@
{
"more": "Plus"
}

View File

@ -0,0 +1,3 @@
{
"more": "עוד"
}

View File

@ -0,0 +1,3 @@
{
"more": "Više"
}

Some files were not shown because too many files have changed in this diff Show More