Import Vector lab project
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import 'leaflet.heat';
|
||||
import './HeatmapView.css';
|
||||
|
||||
const HeatmapView = () => {
|
||||
const mapRef = useRef(null);
|
||||
const mapInstanceRef = useRef(null);
|
||||
const heatLayerRef = useRef(null);
|
||||
|
||||
const [heatmapData, setHeatmapData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
// Filters
|
||||
const [filterType, setFilterType] = useState('all');
|
||||
const [ageGroup, setAgeGroup] = useState('');
|
||||
const [season, setSeason] = useState('');
|
||||
const [yearFrom, setYearFrom] = useState('');
|
||||
const [yearTo, setYearTo] = useState('');
|
||||
|
||||
// Layer visibility
|
||||
const [showHeatmap, setShowHeatmap] = useState(true);
|
||||
|
||||
// Initialize map
|
||||
useEffect(() => {
|
||||
if (!mapRef.current || mapInstanceRef.current) return;
|
||||
|
||||
// Create map centered on Russia
|
||||
const map = L.map(mapRef.current).setView([55.7558, 37.6173], 5);
|
||||
|
||||
// Add OpenStreetMap tiles
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: 18
|
||||
}).addTo(map);
|
||||
|
||||
mapInstanceRef.current = map;
|
||||
|
||||
return () => {
|
||||
if (mapInstanceRef.current) {
|
||||
mapInstanceRef.current.remove();
|
||||
mapInstanceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch heatmap data
|
||||
const fetchHeatmapData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
type: filterType
|
||||
});
|
||||
|
||||
if (filterType === 'age' && ageGroup) {
|
||||
params.append('age_group', ageGroup);
|
||||
}
|
||||
if (filterType === 'season' && season) {
|
||||
params.append('season', season);
|
||||
}
|
||||
if (yearFrom) {
|
||||
params.append('year_from', yearFrom);
|
||||
}
|
||||
if (yearTo) {
|
||||
params.append('year_to', yearTo);
|
||||
}
|
||||
|
||||
const response = await fetch(`http://192.168.0.99:8000/api/v1/stats/heatmap?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch heatmap data');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setHeatmapData(data.points);
|
||||
setTotal(data.total);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Update heatmap layer
|
||||
useEffect(() => {
|
||||
if (!mapInstanceRef.current) return;
|
||||
|
||||
// Remove existing heat layer
|
||||
if (heatLayerRef.current) {
|
||||
mapInstanceRef.current.removeLayer(heatLayerRef.current);
|
||||
heatLayerRef.current = null;
|
||||
}
|
||||
|
||||
// Add new heat layer if visible and data exists
|
||||
if (showHeatmap && heatmapData.length > 0) {
|
||||
const heatData = heatmapData.map(point => [point.lat, point.lon, point.intensity]);
|
||||
|
||||
const heatLayer = L.heatLayer(heatData, {
|
||||
radius: 25,
|
||||
blur: 15,
|
||||
maxZoom: 10,
|
||||
max: 1.0,
|
||||
gradient: {
|
||||
0.0: 'blue',
|
||||
0.5: 'lime',
|
||||
0.7: 'yellow',
|
||||
1.0: 'red'
|
||||
}
|
||||
});
|
||||
|
||||
heatLayer.addTo(mapInstanceRef.current);
|
||||
heatLayerRef.current = heatLayer;
|
||||
}
|
||||
}, [heatmapData, showHeatmap]);
|
||||
|
||||
// Load data on mount and filter change
|
||||
useEffect(() => {
|
||||
fetchHeatmapData();
|
||||
}, [filterType, ageGroup, season, yearFrom, yearTo]);
|
||||
|
||||
const handleFilterTypeChange = (type) => {
|
||||
setFilterType(type);
|
||||
// Reset specific filters
|
||||
if (type !== 'age') setAgeGroup('');
|
||||
if (type !== 'season') setSeason('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="heatmap-view">
|
||||
<div className="heatmap-controls">
|
||||
<h2>Тепловая карта находок</h2>
|
||||
|
||||
<div className="control-group">
|
||||
<label>Тип фильтра:</label>
|
||||
<div className="button-group">
|
||||
<button
|
||||
className={filterType === 'all' ? 'active' : ''}
|
||||
onClick={() => handleFilterTypeChange('all')}
|
||||
>
|
||||
Все
|
||||
</button>
|
||||
<button
|
||||
className={filterType === 'age' ? 'active' : ''}
|
||||
onClick={() => handleFilterTypeChange('age')}
|
||||
>
|
||||
По возрасту
|
||||
</button>
|
||||
<button
|
||||
className={filterType === 'season' ? 'active' : ''}
|
||||
onClick={() => handleFilterTypeChange('season')}
|
||||
>
|
||||
По сезону
|
||||
</button>
|
||||
<button
|
||||
className={filterType === 'outcome' ? 'active' : ''}
|
||||
onClick={() => handleFilterTypeChange('outcome')}
|
||||
>
|
||||
По исходу
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filterType === 'age' && (
|
||||
<div className="control-group">
|
||||
<label>Возрастная группа:</label>
|
||||
<select value={ageGroup} onChange={(e) => setAgeGroup(e.target.value)}>
|
||||
<option value="">Все</option>
|
||||
<option value="0-6">0-6 лет</option>
|
||||
<option value="7-10">7-10 лет</option>
|
||||
<option value="11-14">11-14 лет</option>
|
||||
<option value="15-17">15-17 лет</option>
|
||||
<option value="18+">18+ лет</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filterType === 'season' && (
|
||||
<div className="control-group">
|
||||
<label>Сезон:</label>
|
||||
<select value={season} onChange={(e) => setSeason(e.target.value)}>
|
||||
<option value="">Все</option>
|
||||
<option value="зима">Зима</option>
|
||||
<option value="весна">Весна</option>
|
||||
<option value="лето">Лето</option>
|
||||
<option value="осень">Осень</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="control-group">
|
||||
<label>Период:</label>
|
||||
<div className="year-range">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="От года"
|
||||
value={yearFrom}
|
||||
onChange={(e) => setYearFrom(e.target.value)}
|
||||
min="2000"
|
||||
max="2100"
|
||||
/>
|
||||
<span>—</span>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="До года"
|
||||
value={yearTo}
|
||||
onChange={(e) => setYearTo(e.target.value)}
|
||||
min="2000"
|
||||
max="2100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="control-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showHeatmap}
|
||||
onChange={(e) => setShowHeatmap(e.target.checked)}
|
||||
/>
|
||||
Показать тепловую карту
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="stats-info">
|
||||
<p>Всего точек: <strong>{total}</strong></p>
|
||||
{loading && <p className="loading">Загрузка...</p>}
|
||||
{error && <p className="error">Ошибка: {error}</p>}
|
||||
</div>
|
||||
|
||||
<div className="legend">
|
||||
<h3>Легенда</h3>
|
||||
<div className="legend-gradient">
|
||||
<span className="legend-label">Низкая плотность</span>
|
||||
<div className="gradient-bar"></div>
|
||||
<span className="legend-label">Высокая плотность</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="map-container">
|
||||
<div ref={mapRef} className="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeatmapView;
|
||||
Reference in New Issue
Block a user