@mytec: iter5 ready for test

This commit is contained in:
2026-01-30 12:12:13 +02:00
parent 1a3c3e0f11
commit a03e9746d4
6 changed files with 124 additions and 44 deletions

View File

@@ -30,26 +30,24 @@ function rsrpToIntensity(rsrp: number): number {
}
/**
* Calculate adaptive heatmap params based on zoom level.
* Calculate adaptive heatmap visual params based on zoom level.
*
* radius/blur: smaller at close zoom to avoid blocky squares
* maxIntensity: lower at close zoom so densely packed points
* don't all saturate to a single solid color
* Only radius and blur change with zoom (visual quality).
* maxIntensity is CONSTANT at 1.0 so the same RSRP always
* maps to the same color regardless of zoom level.
*
* Zoom 6 (country): radius=35, blur=18, max=0.90
* Zoom 10 (region): radius=25, blur=13, max=0.70
* Zoom 14 (city): radius=15, blur=9, max=0.50
* Zoom 18 (street): radius=8, blur=6, max=0.30
* Zoom 6 (country): radius=35, blur=18
* Zoom 10 (region): radius=25, blur=13
* Zoom 14 (city): radius=15, blur=9
* Zoom 18 (street): radius=8, blur=6
*/
function getHeatmapParams(zoom: number) {
const radius = Math.max(8, Math.min(40, 50 - zoom * 2.5));
const blur = Math.max(6, Math.min(20, 30 - zoom * 1.5));
// Dynamic max: prevents saturation at close zoom
const maxIntensity = Math.max(0.3, Math.min(1.0, 1.2 - zoom * 0.05));
return {
radius: Math.round(radius),
blur: Math.round(blur),
maxIntensity,
maxIntensity: 1.0, // CONSTANT — zoom-independent colors
};
}

View File

@@ -3,7 +3,11 @@ import { useSitesStore } from '@/store/sites.ts';
import { useToastStore } from '@/components/ui/Toast.tsx';
import Button from '@/components/ui/Button.tsx';
export default function BatchEdit() {
interface BatchEditProps {
onBatchApplied?: (affectedIds: string[]) => void;
}
export default function BatchEdit({ onBatchApplied }: BatchEditProps) {
const selectedSiteIds = useSitesStore((s) => s.selectedSiteIds);
const batchUpdateHeight = useSitesStore((s) => s.batchUpdateHeight);
const batchSetHeight = useSitesStore((s) => s.batchSetHeight);
@@ -15,9 +19,11 @@ export default function BatchEdit() {
if (selectedSiteIds.length === 0) return null;
const handleAdjustHeight = async (delta: number) => {
const ids = [...selectedSiteIds];
await batchUpdateHeight(delta);
onBatchApplied?.(ids);
addToast(
`Adjusted ${selectedSiteIds.length} site(s) by ${delta > 0 ? '+' : ''}${delta}m`,
`Updated ${ids.length} site(s) by ${delta > 0 ? '+' : ''}${delta}m`,
'success'
);
};
@@ -28,8 +34,10 @@ export default function BatchEdit() {
addToast('Height must be between 1-100m', 'error');
return;
}
const ids = [...selectedSiteIds];
await batchSetHeight(height);
addToast(`Set ${selectedSiteIds.length} site(s) to ${height}m`, 'success');
onBatchApplied?.(ids);
addToast(`Set ${ids.length} site(s) to ${height}m`, 'success');
setCustomHeight('');
};

View File

@@ -77,6 +77,24 @@ export default function SiteForm({
}
}, [pendingLocation]);
// Live-sync: update form when the edited site changes externally
// (e.g., batch height adjustment, drag on map)
useEffect(() => {
if (editSite) {
setHeight(editSite.height);
setPower(editSite.power);
setGain(editSite.gain);
setName(editSite.name);
setLat(editSite.lat);
setLon(editSite.lon);
setFrequency(editSite.frequency);
setAntennaType(editSite.antennaType);
setAzimuth(editSite.azimuth ?? 0);
setBeamwidth(editSite.beamwidth ?? 65);
setNotes(editSite.notes ?? '');
}
}, [editSite]);
const applyTemplate = (key: keyof typeof TEMPLATES) => {
const t = TEMPLATES[key];
setName(t.name);

View File

@@ -1,3 +1,4 @@
import { useState, useCallback } from 'react';
import type { Site } from '@/types/index.ts';
import { useSitesStore } from '@/store/sites.ts';
import { useToastStore } from '@/components/ui/Toast.tsx';
@@ -22,6 +23,15 @@ export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
const clearSelection = useSitesStore((s) => s.clearSelection);
const addToast = useToastStore((s) => s.addToast);
// Track recently batch-updated site IDs for flash animation
const [flashIds, setFlashIds] = useState<Set<string>>(new Set());
const triggerFlash = useCallback((ids: string[]) => {
setFlashIds(new Set(ids));
// Clear after animation completes
setTimeout(() => setFlashIds(new Set()), 700);
}, []);
const handleDelete = async (id: string, name: string) => {
await deleteSite(id);
addToast(`"${name}" deleted`, 'info');
@@ -73,7 +83,7 @@ export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
{/* Batch edit panel (appears when sites are selected) */}
{selectedSiteIds.length > 0 && (
<div className="px-3 pt-3">
<BatchEdit />
<BatchEdit onBatchApplied={triggerFlash} />
</div>
)}
@@ -86,6 +96,7 @@ export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
<div className="divide-y divide-gray-100 dark:divide-dark-border max-h-60 overflow-y-auto">
{sites.map((site) => {
const isBatchSelected = selectedSiteIds.includes(site.id);
const isFlashing = flashIds.has(site.id);
return (
<div
@@ -93,7 +104,8 @@ export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
className={`px-4 py-2.5 flex items-center gap-2.5 cursor-pointer
hover:bg-gray-50 dark:hover:bg-dark-border/50 transition-colors
${selectedSiteId === site.id ? 'bg-blue-50 dark:bg-blue-900/20' : ''}
${isBatchSelected && selectedSiteId !== site.id ? 'bg-blue-50/50 dark:bg-blue-900/10' : ''}`}
${isBatchSelected && selectedSiteId !== site.id ? 'bg-blue-50/50 dark:bg-blue-900/10' : ''}
${isFlashing ? 'flash-update' : ''}`}
onClick={() => selectSite(site.id)}
>
{/* Batch checkbox */}