Skip to content
🔵Entwurf (gut)62%
Vollständigkeit:
80%
Korrektheit:
75%
⏳ Noch nicht geprüft

Geoserver Integration

Status: ✅ Vollständig dokumentiert

Übersicht

Die Geoserver Integration in p2d2 ermöglicht den Zugriff auf professionelle Geodaten-Services über standardisierte OGC-Protokolle (WMS, WFS, WFS-T). Die Implementierung bietet sichere Authentifizierung, performantes Layer-Management und robuste Fehlerbehandlung.

Architektur

Service-Integration Overview

WMS Services Integration

Luftbild WMS Layer

typescript
/**
 * Erstellt Luftbild-WMS-Layer für Köln
 * Service: Stadt Köln Luftbilder 2024
 * URL: https://geoportal.stadt-koeln.de/wss/service/luftbilder_2024_wms/guest
 */
export function createLuftbildLayer(projection: string): TileLayer {
  const supportedProjections = ["EPSG:3857", "EPSG:25832"];
  const useProjection = supportedProjections.includes(projection)
    ? projection
    : "EPSG:3857";

  const layer = new TileLayer({
    source: new TileWMS({
      url: "https://geoportal.stadt-koeln.de/wss/service/luftbilder_2024_wms/guest",
      params: {
        LAYERS: "luftbilder_2024_23",
        FORMAT: "image/png",
        TILED: true,
      },
      projection: useProjection,
      crossOrigin: "anonymous",
    }),
    zIndex: MAP_CONFIG.Z_INDEX.LUFTBILD,
    visible: false,
  });

  return layer;
}

Basemap.de WMS Layer

typescript
/**
 * Erstellt basemap.de WMS-Layer
 * Service: Geodatenzentrum basemap.de
 * URL: https://sgx.geodatenzentrum.de/wms_basemapde
 */
export function createBasemapLayer(): TileLayer {
  const layer = new TileLayer({
    source: new TileWMS({
      url: "https://sgx.geodatenzentrum.de/wms_basapde",
      params: {
        LAYERS: "de_basemapde_web_raster_farbe",
        FORMAT: "image/png",
        TRANSPARENT: "true",
        TILED: true,
      },
      projection: "EPSG:3857",
      crossOrigin: "anonymous",
    }),
    zIndex: MAP_CONFIG.Z_INDEX.BASEMAP,
    visible: false,
  });

  return layer;
}

WFS Services Integration

WFS Layer Management

typescript
export class WFSLayerManager {
  private map: OLMap;
  private activeLayer: VectorLayer<VectorSource> | null = null;
  private layerCache = new Map<string, VectorLayer<VectorSource>>();

  /**
   * Zeigt WFS-Layer für Kommune und Kategorie an
   */
  async displayLayer(kommune: KommuneData, categorySlug: string): Promise<void> {
    const config = this.buildLayerConfig(kommune, categorySlug);
    const cacheKey = `${config.wpName}-${config.containerType}-${config.osmAdminLevel}`;

    // Caching für Performance
    let layer = this.layerCache.get(cacheKey);
    if (!layer) {
      layer = await this.createWFSLayer(config);
      this.layerCache.set(cacheKey, layer);
      this.map.addLayer(layer);
    }

    this.activeLayer = layer;
    this.activeLayer.setVisible(true);
  }

  private buildLayerConfig(kommune: KommuneData, categorySlug: string): WFSLayerConfig {
    const containerType = this.getContainerType(categorySlug);
    const osmAdminLevel = this.getOsmAdminLevel(kommune, containerType);

    return {
      wpName: kommune.wp_name,
      containerType,
      osmAdminLevel,
    };
  }
}

WFS Query Builder

typescript
// CQL-Filter für verschiedene Szenarien
const WFS_FILTERS = {
  // Administrative Grenzen
  administrative: (wpName: string, adminLevel: number) => 
    `wp_name='${wpName}' AND container_type='administrative' AND osm_admin_level=${adminLevel}`,
  
  // Friedhöfe
  cemetery: (wpName: string) => 
    `wp_name='${wpName}' AND container_type='cemetery' AND osm_admin_level=8`,
  
  // BBox-basierte Abfragen
  bbox: (bbox: number[], crs: string = "EPSG:4326") => 
    `BBOX(geometry,${bbox.join(',')},'${crs}')`
};

// WFS-URL Konstruktion
function buildWFSURL(typeName: string, params: Record<string, string>): string {
  const baseParams = {
    service: "WFS",
    version: "2.0.0",
    request: "GetFeature",
    typeName: `Verwaltungsdaten:${typeName}`,
    outputFormat: "application/json",
    srsName: "EPSG:4326",
    ...params
  };

  const queryString = Object.entries(baseParams)
    .map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`)
    .join("&");

  return `https://wfs.data-dna.eu/geoserver/ows?${queryString}`;
}

Authentication & Security

WFS Auth Client

typescript
export class WFSAuthClient {
  private config: WFSConfig;

  constructor(config: Partial<WFSConfig> = {}) {
    this.config = {
      endpoint: config.endpoint || "https://wfs.data-dna.eu/geoserver/ows",
      workspace: config.workspace || "Verwaltungsdaten",
      namespace: config.namespace || "urn:data-dna:govdata",
      credentials: {
        username: config.credentials?.username || "p2d2_wfs_user",
        password: config.credentials?.password || "eif1nu4ao9Loh0oobeev"
      },
    };
  }

  /**
   * Erstellt autorisierte WFS-URL mit Proxy
   */
  buildAuthorizedWFSURL(typeName: string, params: Record<string, string> = {}): string {
    const baseParams = {
      service: "WFS",
      version: "2.0.0",
      request: "GetFeature",
      typeName: `${this.config.workspace}:${typeName}`,
      outputFormat: "application/json",
      srsName: "EPSG:4326",
      ...params
    };

    const queryString = Object.entries(baseParams)
      .map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`)
      .join("&");

    const wfsUrl = `${this.config.endpoint}?${queryString}`;
    
    // Proxy für CORS und Authentifizierung
    const encodedWfsUrl = encodeURIComponent(wfsUrl);
    return `/api/wfs-proxy?url=${encodedWfsUrl}`;
  }
}

Security Features

typescript
// WFS Proxy Security
const allowedHosts = ["wfs.data-dna.eu", "ows.data-dna.eu"];

function validateWFSURL(url: string): boolean {
  try {
    const urlHost = new URL(url).hostname;
    return allowedHosts.includes(urlHost);
  } catch {
    return false;
  }
}

// Credential Management
function getWFSCredentials(): WFSCredentials {
  // Bevorzuge Environment-Variablen
  return {
    username: import.meta.env.WFS_USERNAME || "p2d2_wfs_user",
    password: import.meta.env.WFS_PASSWORD || "eif1nu4ao9Loh0oobeev"
  };
}

Layer Styling & Configuration

Vector Layer Styles

typescript
// Standard-Styles für WFS-Vektorlayer
const WFS_LAYER_STYLES = {
  default: new Style({
    stroke: new Stroke({
      color: "#FF6900",  // p2d2 Orange
      width: 2,
    }),
    fill: new Fill({
      color: "rgba(255, 105, 0, 0.1)",
    }),
  }),
  
  highlighted: new Style({
    stroke: new Stroke({
      color: "#FF4500",
      width: 3,
    }),
    fill: new Fill({
      color: "rgba(255, 69, 0, 0.2)",
    }),
  }),
  
  selected: new Style({
    stroke: new Stroke({
      color: "#DC143C",  // Rot für Selektion
      width: 4,
    }),
    fill: new Fill({
      color: "rgba(220, 20, 60, 0.3)",
    }),
  })
};

// Dynamisches Styling basierend auf Feature-Eigenschaften
function getStyleForFeature(feature: Feature): Style {
  const properties = feature.getProperties();
  
  if (properties.container_type === 'cemetery') {
    return new Style({
      stroke: new Stroke({
        color: '#8B4513',  // Braun für Friedhöfe
        width: 2,
      }),
      fill: new Fill({
        color: 'rgba(139, 69, 19, 0.1)',
      }),
    });
  }
  
  return WFS_LAYER_STYLES.default;
}

Performance Optimizations

Layer Caching

typescript
class CachedWFSLayerManager extends WFSLayerManager {
  private layerCache = new Map<string, VectorLayer<VectorSource>>();
  private cacheHits = 0;
  private cacheMisses = 0;

  async getCachedLayer(config: WFSLayerConfig): Promise<VectorLayer<VectorSource>> {
    const cacheKey = this.generateCacheKey(config);
    
    if (this.layerCache.has(cacheKey)) {
      this.cacheHits++;
      return this.layerCache.get(cacheKey)!;
    }
    
    this.cacheMisses++;
    const layer = await this.createWFSLayer(config);
    this.layerCache.set(cacheKey, layer);
    
    return layer;
  }

  getCacheStats() {
    const total = this.cacheHits + this.cacheMisses;
    const hitRate = total > 0 ? (this.cacheHits / total) * 100 : 0;
    
    return {
      hits: this.cacheHits,
      misses: this.cacheMisses,
      hitRate: `${hitRate.toFixed(1)}%`,
      cacheSize: this.layerCache.size
    };
  }
}

Request Batching

typescript
// Batch-Verarbeitung für mehrere WFS-Requests
async function batchWFSRequests(requests: WFSRequest[]): Promise<WFSResult[]> {
  const batches = chunkArray(requests, 5); // 5 Requests pro Batch
  
  const results: WFSResult[] = [];
  
  for (const batch of batches) {
    const batchPromises = batch.map(req => 
      wfsAuthClient.getFeatures(req.typeName, req.params)
    );
    
    const batchResults = await Promise.allSettled(batchPromises);
    
    batchResults.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        results.push({
          request: batch[index],
          data: result.value,
          success: true
        });
      } else {
        results.push({
          request: batch[index],
          error: result.reason,
          success: false
        });
      }
    });
    
    // Kurze Pause zwischen Batches
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  
  return results;
}

Error Handling & Retry Logic

Robust WFS Operations

typescript
async function resilientWFSOperation<T>(
  operation: string,
  fn: () => Promise<T>,
  options: {
    maxRetries?: number;
    retryDelay?: number;
    timeout?: number;
  } = {}
): Promise<T> {
  const { maxRetries = 3, retryDelay = 1000, timeout = 30000 } = options;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), timeout);
      
      const result = await fn();
      
      clearTimeout(timeoutId);
      return result;
      
    } catch (error) {
      console.warn(`${operation} fehlgeschlagen (Versuch ${attempt + 1}/${maxRetries + 1})`, error);
      
      if (attempt === maxRetries) {
        throw new Error(`${operation} nach ${maxRetries + 1} Versuchen fehlgeschlagen: ${error.message}`);
      }
      
      // Exponentielles Backoff
      await new Promise(resolve => 
        setTimeout(resolve, retryDelay * Math.pow(2, attempt))
      );
    }
  }
  
  throw new Error('Unreachable code');
}

Graceful Degradation

typescript
// Fallback-Strategien bei Geoserver-Ausfall
async function loadGeodataWithFallback(
  kommune: KommuneData,
  category: string
): Promise<any> {
  try {
    // Primär: Geoserver WFS
    return await wfsManager.displayLayer(kommune, category);
    
  } catch (error) {
    console.error("Geoserver nicht verfügbar, verwende Fallbacks", error);
    
    // Fallback 1: Lokale GeoJSON-Daten
    const localData = await loadLocalGeoJSON(kommune.slug, category);
    if (localData) {
      return displayLocalData(localData);
    }
    
    // Fallback 2: Statische Grenzen
    const staticData = await loadStaticBoundaries(kommune);
    if (staticData) {
      return displayStaticData(staticData);
    }
    
    // Fallback 3: Benutzer informieren
    showUserNotification({
      type: "warning",
      message: "Geodaten vorübergehend nicht verfügbar",
      details: "Bitte versuchen Sie es später erneut",
      action: "retry"
    });
    
    throw error;
  }
}

Monitoring & Logging

Performance Monitoring

typescript
interface GeoserverMetrics {
  requestCount: number;
  successCount: number;
  errorCount: number;
  averageResponseTime: number;
  endpointUsage: Record<string, number>;
}

class GeoserverMonitor {
  private metrics: GeoserverMetrics = {
    requestCount: 0,
    successCount: 0,
    errorCount: 0,
    averageResponseTime: 0,
    endpointUsage: {}
  };

  trackRequest(endpoint: string, startTime: number, success: boolean, error?: string) {
    this.metrics.requestCount++;
    
    if (success) {
      this.metrics.successCount++;
      const duration = Date.now() - startTime;
      this.updateAverageTime(duration);
    } else {
      this.metrics.errorCount++;
    }
    
    this.metrics.endpointUsage[endpoint] = (this.metrics.endpointUsage[endpoint] || 0) + 1;
    
    // Logging für Debugging
    if (process.env.DEBUG) {
      console.debug('Geoserver Request:', {
        endpoint,
        success,
        duration: Date.now() - startTime,
        error
      });
    }
  }

  getMetrics(): GeoserverMetrics {
    const successRate = this.metrics.requestCount > 0 
      ? (this.metrics.successCount / this.metrics.requestCount) * 100 
      : 0;
    
    return {
      ...this.metrics,
      successRate: `${successRate.toFixed(1)}%`
    };
  }
}

Configuration

Environment Configuration

typescript
const GEOSERVER_CONFIG = {
  development: {
    wfs: {
      endpoint: "https://wfs.data-dna.eu/geoserver/ows",
      workspace: "Verwaltungsdaten",
      timeout: 30000,
      maxRetries: 3
    },
    wms: {
      endpoints: {
        luftbild: "https://geoportal.stadt-koeln.de/wss/service/luftbilder_2024_wms/guest",
        basemap: "https://sgx.geodatenzentrum.de/wms_basemapde"
      }
    }
  },
  production: {
    wfs: {
      endpoint: "https://wfs.data-dna.eu/geoserver/Verwaltungsdaten/ows",
      workspace: "Verwaltungsdaten",
      timeout: 60000,
      maxRetries: 5
    },
    wms: {
      endpoints: {
        luftbild: "https://geoportal.stadt-koeln.de/wss/service/luftbilder_2024_wms/guest",
        basemap: "https://sgx.geodatenzentrum.de/wms_basemapde"
      }
    }
  }
};

Layer Configuration

typescript
// Standard-Layer-Konfiguration
const LAYER_CONFIGS = {
  administrative: {
    containerType: "administrative",
    style: WFS_LAYER_STYLES.default,
    minZoom: 8,
    maxZoom: 18