stations, String apiKey) {
+ this.stations = stations;
+ this.apiKey = apiKey;
+
+ setHeaderTitle("Tour-Übersicht");
+ setWidth("80vw");
+ setHeight("85vh");
+ setCloseOnOutsideClick(true);
+
+ add(createContent());
+ createFooter();
+ }
+
+ private VerticalLayout createContent() {
+ VerticalLayout layout = new VerticalLayout();
+ layout.setSizeFull();
+ layout.setPadding(false);
+ layout.setSpacing(true);
+
+ // Info section
+ HorizontalLayout infoSection = createInfoSection();
+ layout.add(infoSection);
+
+ // Map container
+ Div mapContainer = createMapContainer();
+ layout.add(mapContainer);
+ layout.setFlexGrow(1, mapContainer);
+
+ return layout;
+ }
+
+ private HorizontalLayout createInfoSection() {
+ HorizontalLayout layout = new HorizontalLayout();
+ layout.setWidthFull();
+ layout.setSpacing(true);
+ layout.setAlignItems(FlexComponent.Alignment.CENTER);
+
+ // Distance info
+ Div distanceBox = new Div();
+ distanceBox.addClassNames(
+ LumoUtility.Background.PRIMARY_10,
+ LumoUtility.BorderRadius.MEDIUM,
+ LumoUtility.Padding.MEDIUM
+ );
+ VerticalLayout distanceContent = new VerticalLayout();
+ distanceContent.setPadding(false);
+ distanceContent.setSpacing(false);
+ Span distanceTitle = new Span("Entfernung");
+ distanceTitle.addClassNames(LumoUtility.FontSize.SMALL, LumoUtility.TextColor.SECONDARY);
+ distanceLabel = new Span("Wird berechnet...");
+ distanceLabel.addClassNames(LumoUtility.FontSize.XLARGE, LumoUtility.FontWeight.BOLD);
+ distanceContent.add(distanceTitle, distanceLabel);
+ distanceBox.add(distanceContent);
+
+ // Duration info
+ Div durationBox = new Div();
+ durationBox.addClassNames(
+ LumoUtility.Background.SUCCESS_10,
+ LumoUtility.BorderRadius.MEDIUM,
+ LumoUtility.Padding.MEDIUM
+ );
+ VerticalLayout durationContent = new VerticalLayout();
+ durationContent.setPadding(false);
+ durationContent.setSpacing(false);
+ Span durationTitle = new Span("Geschätzte Fahrzeit");
+ durationTitle.addClassNames(LumoUtility.FontSize.SMALL, LumoUtility.TextColor.SECONDARY);
+ durationLabel = new Span("Wird berechnet...");
+ durationLabel.addClassNames(LumoUtility.FontSize.XLARGE, LumoUtility.FontWeight.BOLD);
+ durationContent.add(durationTitle, durationLabel);
+ durationBox.add(durationContent);
+
+ // Stations info
+ Div stationsBox = new Div();
+ stationsBox.addClassNames(
+ LumoUtility.Background.CONTRAST_5,
+ LumoUtility.BorderRadius.MEDIUM,
+ LumoUtility.Padding.MEDIUM
+ );
+ VerticalLayout stationsContent = new VerticalLayout();
+ stationsContent.setPadding(false);
+ stationsContent.setSpacing(false);
+ Span stationsTitle = new Span("Stationen");
+ stationsTitle.addClassNames(LumoUtility.FontSize.SMALL, LumoUtility.TextColor.SECONDARY);
+ Span stationsCount = new Span(String.valueOf(stations.size()));
+ stationsCount.addClassNames(LumoUtility.FontSize.XLARGE, LumoUtility.FontWeight.BOLD);
+ stationsContent.add(stationsTitle, stationsCount);
+ stationsBox.add(stationsContent);
+
+ layout.add(distanceBox, durationBox, stationsBox);
+ return layout;
+ }
+
+ private Div createMapContainer() {
+ Div mapDiv = new Div();
+ mapDiv.setId("tour-map");
+ mapDiv.setSizeFull();
+ mapDiv.getStyle()
+ .set("min-height", "400px")
+ .set("border-radius", "var(--lumo-border-radius-m)")
+ .set("overflow", "hidden");
+
+ // Build waypoints JavaScript array
+ String waypointsJs = stations.stream()
+ .map(s -> {
+ String addr = s.getAddress() != null ? s.getAddress() : s.getName();
+ return "\"" + escapeJs(addr) + "\"";
+ })
+ .collect(Collectors.joining(", "));
+
+ // JavaScript to initialize Google Maps
+ String initScript = """
+ (function() {
+ const mapDiv = document.getElementById('tour-map');
+ if (!mapDiv) return;
+
+ // Load Google Maps script if not already loaded
+ if (!window.google || !window.google.maps) {
+ const script = document.createElement('script');
+ script.src = 'https://maps.googleapis.com/maps/api/js?key=%s&libraries=places';
+ script.async = true;
+ script.defer = true;
+ script.onload = function() {
+ initMap();
+ };
+ document.head.appendChild(script);
+ } else {
+ initMap();
+ }
+
+ function initMap() {
+ const waypoints = [%s];
+ if (waypoints.length < 2) {
+ mapDiv.innerHTML = 'Mindestens 2 Stationen erforderlich
';
+ return;
+ }
+
+ const map = new google.maps.Map(mapDiv, {
+ zoom: 10,
+ center: { lat: 51.1657, lng: 10.4515 }, // Germany center
+ mapTypeControl: true,
+ streetViewControl: false,
+ fullscreenControl: true
+ });
+
+ const directionsService = new google.maps.DirectionsService();
+ const directionsRenderer = new google.maps.DirectionsRenderer({
+ map: map,
+ suppressMarkers: false,
+ polylineOptions: {
+ strokeColor: '#1676F3',
+ strokeWeight: 5
+ }
+ });
+
+ const origin = waypoints[0];
+ const destination = waypoints[waypoints.length - 1];
+ const middleWaypoints = waypoints.slice(1, -1).map(wp => ({
+ location: wp,
+ stopover: true
+ }));
+
+ directionsService.route({
+ origin: origin,
+ destination: destination,
+ waypoints: middleWaypoints,
+ travelMode: google.maps.TravelMode.DRIVING,
+ region: 'de'
+ }, function(result, status) {
+ if (status === 'OK') {
+ directionsRenderer.setDirections(result);
+
+ // Calculate total distance and duration
+ let totalDistance = 0;
+ let totalDuration = 0;
+ result.routes[0].legs.forEach(leg => {
+ totalDistance += leg.distance.value;
+ totalDuration += leg.duration.value;
+ });
+
+ const distanceKm = (totalDistance / 1000).toFixed(1);
+ const hours = Math.floor(totalDuration / 3600);
+ const minutes = Math.floor((totalDuration %% 3600) / 60);
+ const durationText = hours > 0 ? hours + ' h ' + minutes + ' min' : minutes + ' min';
+
+ // Update the labels via server
+ const dialogElement = mapDiv.closest('vaadin-dialog-overlay');
+ if (dialogElement && dialogElement.__component) {
+ dialogElement.__component.$server.updateRouteInfo(distanceKm, durationText);
+ }
+
+ // Also update directly via DOM as fallback
+ const distanceSpan = document.querySelector('[data-distance-label]');
+ const durationSpan = document.querySelector('[data-duration-label]');
+ if (distanceSpan) distanceSpan.textContent = distanceKm + ' km';
+ if (durationSpan) durationSpan.textContent = durationText;
+ } else {
+ console.error('Directions request failed:', status);
+ mapDiv.innerHTML = 'Route konnte nicht berechnet werden: ' + status + '
';
+ }
+ });
+ }
+ })();
+ """.formatted(apiKey, waypointsJs);
+
+ // Add data attributes for JavaScript fallback update
+ distanceLabel.getElement().setAttribute("data-distance-label", "true");
+ durationLabel.getElement().setAttribute("data-duration-label", "true");
+
+ // Execute script after attach
+ mapDiv.getElement().executeJs(initScript);
+
+ return mapDiv;
+ }
+
+ private String escapeJs(String str) {
+ if (str == null) return "";
+ return str.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r");
+ }
+
+ @ClientCallable
+ public void updateRouteInfo(String distance, String duration) {
+ getUI().ifPresent(ui -> ui.access(() -> {
+ distanceLabel.setText(distance + " km");
+ durationLabel.setText(duration);
+ }));
+ }
+
+ private void createFooter() {
+ Button closeButton = new Button("Schließen", e -> close());
+ getFooter().add(closeButton);
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index b145b25..da0ff76 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -5,5 +5,38 @@ logging.level.org.atmosphere=warn
vaadin.launch-browser=true
# To improve the performance during development.
-# For more information https://vaadin.com/docs/latest/flow/integrations/spring/configuration#special-configuration-parameters
vaadin.allowed-packages=com.vaadin,org.vaadin,com.flowingcode,de.assecutor.aimailassistant
+
+# H2 Database Configuration
+spring.datasource.url=jdbc:h2:mem:mailassistant
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+spring.jpa.hibernate.ddl-auto=create-drop
+spring.h2.console.enabled=true
+
+# IMAP Configuration
+mail.imap.host=mail.appcreation.de
+mail.imap.port=993
+mail.imap.username=sb@appcreation.de
+mail.imap.password=SV1705CA!sb
+mail.imap.ssl=true
+mail.imap.folder=INBOX
+mail.imap.poll-interval-seconds=60
+
+# SMTP Configuration
+spring.mail.host=mail.appcreation.de
+spring.mail.port=465
+spring.mail.username=sb@appcreation.de
+spring.mail.password=SV1705CA!sb
+spring.mail.properties.mail.smtp.auth=true
+spring.mail.properties.mail.smtp.ssl.enable=true
+spring.mail.properties.mail.smtp.ssl.required=true
+
+# LM Studio Configuration
+llm.api.url=http://192.168.180.10:1234/v1/chat/completions
+llm.api.model=local-model
+
+# Google Maps Configuration
+google.maps.api.key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE