Files
Investidor-V2/resources/js/pages/investment/zone-map.tsx
T
nala 1106509116
tests / ci (push) Canceled after 0s
vrsao 2
2026-10-08 14:02:01 +02:00

126 lines
5.2 KiB
TypeScript

import { useState } from 'react';
import { api, button, card } from './shared';
type Feature = {
id: string;
geometry: { type: string; coordinates: unknown[] };
properties: { purpose: string; source: string; observed_at: string };
};
function rings(value: unknown): number[][][] {
if (!Array.isArray(value) || !value.length) return [];
if (typeof value[0] === 'number') return [[value as number[]]];
if (Array.isArray(value[0]) && typeof value[0][0] === 'number')
return [value as number[][]];
return value.flatMap(rings);
}
export default function ZoneMap({ id }: { id: string }) {
const [features, setFeatures] = useState<Feature[] | null>(null);
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
const points = (features ?? []).flatMap((f) =>
rings(f.geometry.coordinates).flat(),
);
const xs = points.map((p) => p[0]),
ys = points.map((p) => p[1]);
const west = xs.length ? Math.min(...xs) : 0,
south = ys.length ? Math.min(...ys) : 0;
const dx = xs.length ? Math.max(...xs) - west : 1,
dy = ys.length ? Math.max(...ys) - south : 1;
const project = (p: number[]) => [
20 + ((p[0] - west) / (dx || 1)) * 600,
300 - ((p[1] - south) / (dy || 1)) * 280,
];
return (
<section className={card}>
<h2 className="text-xl font-bold">
Localização e geometrias autorizadas
</h2>
<button
className={button}
disabled={busy}
onClick={async () => {
setBusy(true);
setError('');
try {
const data = await api(
`/api/v2/investment/zone-map?zone_id=${id}`,
);
setFeatures(data.features);
if (data.truncated)
setError('Exibidas as primeiras 100 geometrias.');
} catch {
setError(
'Mapa indisponível. A ficha, a localização textual e as acções de apoio continuam disponíveis.',
);
} finally {
setBusy(false);
}
}}
>
{busy ? 'A carregar…' : 'Consultar mapa'}
</button>
{error && <p role="status">{error}</p>}
{features?.length === 0 && (
<p>
Não existe geometria autorizada para divulgação nesta ficha.
</p>
)}
{!!features?.length && (
<>
<p>
Esquema das geometrias cadastradas, sem mapa-base. Não
serve para demarcação ou atribuição de estatuto.
</p>
<svg
viewBox="0 0 640 320"
className="w-full rounded border bg-slate-50"
role="img"
aria-label="Esquema das geometrias autorizadas da zona"
>
{features.map((f) => (
<g key={f.id}>
<title>
{f.properties.purpose} ·{' '}
{f.properties.source}
</title>
{rings(f.geometry.coordinates).map((ring, i) =>
ring.length === 1 ? (
<circle
key={i}
cx={project(ring[0])[0]}
cy={project(ring[0])[1]}
r="5"
fill="#065f46"
/>
) : (
<polyline
key={i}
points={ring
.map((p) =>
project(p).join(','),
)
.join(' ')}
fill="none"
stroke="#065f46"
strokeWidth="2"
/>
),
)}
</g>
))}
</svg>
<ul>
{features.map((f) => (
<li key={f.id}>
{f.properties.purpose} · Fonte:{' '}
{f.properties.source} · Data:{' '}
{f.properties.observed_at}
</li>
))}
</ul>
</>
)}
</section>
);
}