126 lines
5.2 KiB
TypeScript
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>
|
|
);
|
|
}
|