1. Kartendarstellung (Neutralisierung):
* Die TileLayer-URL in heatmap-tool/frontend/src/components/MapDisplay.tsx wurde auf eine neutrale CARTO light_all-Kachelansicht umgestellt und die Quellenangabe entsprechend angepasst.
193 lines
6.9 KiB
TypeScript
193 lines
6.9 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import axios from 'axios';
|
|
import './App.css';
|
|
import 'react-leaflet-cluster/dist/assets/MarkerCluster.css';
|
|
import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css';
|
|
import FileUpload from './components/FileUpload';
|
|
import FilterPanel from './components/FilterPanel';
|
|
import MapDisplay from './components/MapDisplay';
|
|
import ErrorBoundary from './components/ErrorBoundary';
|
|
import PlzSelector from './components/PlzSelector';
|
|
|
|
// Define types for our state
|
|
export interface FilterOptions {
|
|
[key: string]: string[];
|
|
}
|
|
|
|
export interface TooltipColumn {
|
|
id: string;
|
|
name: string;
|
|
visible: boolean;
|
|
}
|
|
|
|
export interface HeatmapPoint {
|
|
plz: string;
|
|
lat: number;
|
|
lon: number;
|
|
count: number;
|
|
attributes_summary?: Record<string, string[]>;
|
|
}
|
|
|
|
export type MapMode = 'points' | 'heatmap';
|
|
|
|
function App() {
|
|
const [filters, setFilters] = useState<FilterOptions>({});
|
|
const [heatmapData, setHeatmapData] = useState<HeatmapPoint[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [radiusMultiplier, setRadiusMultiplier] = useState(1);
|
|
const [viewMode, setViewMode] = useState<MapMode>('points');
|
|
|
|
const [plzColumnNeeded, setPlzColumnNeeded] = useState(false);
|
|
const [availableColumns, setAvailableColumns] = useState<string[]>([]);
|
|
const [tooltipColumns, setTooltipColumns] = useState<TooltipColumn[]>([]);
|
|
|
|
|
|
const handleUploadSuccess = (response: any) => {
|
|
setError(null);
|
|
if (response.plz_column_needed) {
|
|
setAvailableColumns(response.columns);
|
|
setPlzColumnNeeded(true);
|
|
setFilters({});
|
|
setHeatmapData([]);
|
|
setTooltipColumns([]);
|
|
} else {
|
|
setPlzColumnNeeded(false);
|
|
const newFilters = response.filters;
|
|
setFilters(newFilters);
|
|
// Initialize tooltip columns based on filters
|
|
setTooltipColumns(Object.keys(newFilters).map(name => ({ id: name, name, visible: true })));
|
|
setHeatmapData([]); // Clear previous heatmap data
|
|
// Automatically fetch data with no filters on successful upload
|
|
// Pass initial tooltip config
|
|
handleFilterChange({}, Object.keys(newFilters).map(name => ({ id: name, name, visible: true })));
|
|
}
|
|
};
|
|
|
|
const handlePlzColumnSubmit = async (selectedColumn: string) => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await axios.post('/heatmap/api/set-plz-column', {
|
|
plz_column: selectedColumn,
|
|
});
|
|
handleUploadSuccess(response.data); // Re-use the success handler
|
|
} catch (error: any) {
|
|
if (axios.isAxiosError(error) && error.response) {
|
|
setError(`Failed to set PLZ column: ${error.response.data.detail || error.message}`);
|
|
}
|
|
else {
|
|
setError(`Failed to set PLZ column: ${error.message}`);
|
|
}
|
|
}
|
|
finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleFilterChange = async (selectedFilters: FilterOptions, currentTooltipConfig: TooltipColumn[]) => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await axios.post('/heatmap/api/heatmap', {
|
|
filters: selectedFilters,
|
|
tooltip_config: currentTooltipConfig, // Pass tooltip config to backend
|
|
});
|
|
setHeatmapData(response.data);
|
|
} catch (error: any) {
|
|
if (axios.isAxiosError(error) && error.response) {
|
|
setError(`Failed to fetch heatmap data: ${error.response.data.detail || error.message}`);
|
|
}
|
|
else {
|
|
setError(`Failed to fetch heatmap data: ${error.message}`);
|
|
}
|
|
setHeatmapData([]); // Clear data on error
|
|
}
|
|
finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// Need to re-fetch data when tooltip config changes.
|
|
// Optimization: In a real app, we might debounce this or add an "Apply" button for sorting.
|
|
// For now, let's keep it manual via the "Apply Filters" button in the FilterPanel to avoid excessive API calls while dragging.
|
|
// The FilterPanel calls onFilterChange when "Apply" is clicked, passing both filters and the current tooltipColumns.
|
|
|
|
return (
|
|
<ErrorBoundary>
|
|
<div className="App">
|
|
<header className="App-header">
|
|
<h1>German PLZ Heatmap Tool</h1>
|
|
</header>
|
|
<main className="App-main">
|
|
<div className="control-panel">
|
|
<FileUpload
|
|
onUploadSuccess={handleUploadSuccess}
|
|
setIsLoading={setIsLoading}
|
|
setError={setError}
|
|
/>
|
|
{plzColumnNeeded ? (
|
|
<PlzSelector
|
|
columns={availableColumns}
|
|
onSubmit={handlePlzColumnSubmit}
|
|
isLoading={isLoading}
|
|
/>
|
|
) : (
|
|
<>
|
|
<FilterPanel
|
|
filters={filters}
|
|
tooltipColumns={tooltipColumns}
|
|
setTooltipColumns={setTooltipColumns}
|
|
onFilterChange={(selectedFilters) => handleFilterChange(selectedFilters, tooltipColumns)}
|
|
isLoading={isLoading}
|
|
/>
|
|
<div className="map-controls" style={{ marginTop: '20px', paddingTop: '20px', borderTop: '1px solid #555' }}>
|
|
<h3>Map Settings</h3>
|
|
<div className="toggle-switch" style={{ marginBottom: '15px' }}>
|
|
<label>
|
|
<input type="radio" value="points" checked={viewMode === 'points'} onChange={() => setViewMode('points')} />
|
|
Points
|
|
</label>
|
|
<label>
|
|
<input type="radio" value="heatmap" checked={viewMode === 'heatmap'} onChange={() => setViewMode('heatmap')} />
|
|
Heatmap
|
|
</label>
|
|
</div>
|
|
<label htmlFor="radius-slider">Marker Size: {radiusMultiplier.toFixed(1)}x</label>
|
|
<input
|
|
type="range"
|
|
id="radius-slider"
|
|
min="0.1"
|
|
max="5"
|
|
step="0.1"
|
|
value={radiusMultiplier}
|
|
onChange={(e) => setRadiusMultiplier(parseFloat(e.target.value))}
|
|
style={{ width: '100%' }}
|
|
disabled={viewMode === 'heatmap'}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="map-container">
|
|
{isLoading ? (
|
|
<p>Loading map data...</p>
|
|
) : error ? (
|
|
<p className="error">{error}</p>
|
|
) : (
|
|
<MapDisplay
|
|
heatmapData={heatmapData}
|
|
radiusMultiplier={radiusMultiplier}
|
|
viewMode={viewMode}
|
|
tooltipColumns={tooltipColumns}
|
|
/>
|
|
)}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|
|
|
|
export default App;
|