alexp97 commited on
Commit
d17e4f4
·
1 Parent(s): 2cce711

feat(ui): click en parcela vuela a su polígono + dibuja parcelas en el mapa

Browse files

- /api/fields incluye geojson (ST_AsGeoJSON) por parcela.
- Mapa de Creación de Parcelas dibuja los polígonos guardados (capa verde) y hace
fitBounds al cargar; click en un item de la lista -> flyToBounds del polígono.
- Verificado con Playwright (click en 'Lote NDVI' vuela a su polígono; 0 errores).

backend/api/fields.py CHANGED
@@ -20,6 +20,8 @@ Ejemplo de Integración:
20
 
21
  from __future__ import annotations
22
 
 
 
23
  from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
24
  from sqlalchemy.ext.asyncio import AsyncSession
25
 
@@ -47,7 +49,16 @@ async def create_field(
47
  async def list_fields(session: AsyncSession = Depends(get_db)) -> list[dict]:
48
  """Lista las parcelas registradas."""
49
  rows = await parcels.list_parcels(session)
50
- return [{"id": str(r.id), "name": r.name, "lon": r.lon, "lat": r.lat} for r in rows]
 
 
 
 
 
 
 
 
 
51
 
52
 
53
  @router.get("/{field_id}")
 
20
 
21
  from __future__ import annotations
22
 
23
+ import json
24
+
25
  from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
26
  from sqlalchemy.ext.asyncio import AsyncSession
27
 
 
49
  async def list_fields(session: AsyncSession = Depends(get_db)) -> list[dict]:
50
  """Lista las parcelas registradas."""
51
  rows = await parcels.list_parcels(session)
52
+ return [
53
+ {
54
+ "id": str(r.id),
55
+ "name": r.name,
56
+ "lon": r.lon,
57
+ "lat": r.lat,
58
+ "geojson": json.loads(r.geojson) if r.geojson else None,
59
+ }
60
+ for r in rows
61
+ ]
62
 
63
 
64
  @router.get("/{field_id}")
backend/db/repositories.py CHANGED
@@ -121,7 +121,7 @@ async def list_fields(session: AsyncSession, user_id: str | None = None) -> list
121
  """Lista las parcelas (filtradas por usuario si se indica), ordenadas por nombre."""
122
  cols = (
123
  "select id, name, ST_X(ST_Centroid(geom)) as lon, "
124
- "ST_Y(ST_Centroid(geom)) as lat from fields"
125
  )
126
  if user_id is None:
127
  result = await session.execute(text(f"{cols} order by name"))
 
121
  """Lista las parcelas (filtradas por usuario si se indica), ordenadas por nombre."""
122
  cols = (
123
  "select id, name, ST_X(ST_Centroid(geom)) as lon, "
124
+ "ST_Y(ST_Centroid(geom)) as lat, ST_AsGeoJSON(geom) as geojson from fields"
125
  )
126
  if user_id is None:
127
  result = await session.execute(text(f"{cols} order by name"))
frontend/src/pages/index.astro CHANGED
@@ -289,7 +289,7 @@ import "../styles/global.css";
289
  let creds = {};
290
  let parcels = [];
291
  let drawnGeojson = null;
292
- let map = null, drawnItems = null;
293
  let chartResumen = null, chartSatelite = null;
294
  const sessionId = "sess-" + Math.random().toString(36).slice(2);
295
 
@@ -389,8 +389,9 @@ import "../styles/global.css";
389
  ? parcels.map((p) => `<option value="${p.id}">${p.name}</option>`).join("")
390
  : '<option value="">(sin parcelas)</option>';
391
  $("parcelas-list").innerHTML = parcels.length
392
- ? parcels.map((p) => `<li class="flex items-center justify-between bg-stone-50 border border-stone-200 rounded-lg px-3 py-2"><span class="font-semibold text-stone-700"><i class="fa-solid fa-location-dot text-emerald-600 mr-1"></i>${p.name}</span></li>`).join("")
393
  : '<li class="text-stone-500">Sin parcelas aún.</li>';
 
394
  }
395
  function initMap() {
396
  if (map) { map.invalidateSize(); return; }
@@ -400,6 +401,11 @@ import "../styles/global.css";
400
  map = L.map("map", { center: [-11.37, -77.37], zoom: 11, layers: [sat] }); // Lomas de Lachay
401
  L.control.layers({ "Satélite": sat, "Normal": osm, "Relieve": topo }).addTo(map);
402
  drawnItems = new L.FeatureGroup().addTo(map);
 
 
 
 
 
403
  const drawControl = new L.Control.Draw({
404
  draw: { polygon: true, marker: false, polyline: false, circle: false, rectangle: false, circlemarker: false },
405
  edit: { featureGroup: drawnItems, edit: false, remove: true },
@@ -410,6 +416,21 @@ import "../styles/global.css";
410
  drawnGeojson = e.layer.toGeoJSON().geometry;
411
  });
412
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  $("btn-save-parcela").addEventListener("click", async () => {
414
  const name = $("parcela-name").value.trim();
415
  if (!drawnGeojson) return notify("Falta el polígono", "Dibuja un polígono en el mapa.");
@@ -430,6 +451,12 @@ import "../styles/global.css";
430
  } catch (e) { notify("Error de red", String(e)); }
431
  });
432
 
 
 
 
 
 
 
433
  // ===== Resumen =====
434
  async function loadResumen() {
435
  const fid = $("select-parcela").value;
 
289
  let creds = {};
290
  let parcels = [];
291
  let drawnGeojson = null;
292
+ let map = null, drawnItems = null, parcelsLayer = null;
293
  let chartResumen = null, chartSatelite = null;
294
  const sessionId = "sess-" + Math.random().toString(36).slice(2);
295
 
 
389
  ? parcels.map((p) => `<option value="${p.id}">${p.name}</option>`).join("")
390
  : '<option value="">(sin parcelas)</option>';
391
  $("parcelas-list").innerHTML = parcels.length
392
+ ? parcels.map((p) => `<li data-pid="${p.id}" title="Ir a la parcela" class="flex items-center justify-between bg-stone-50 border border-stone-200 rounded-lg px-3 py-2 cursor-pointer hover:bg-stone-100"><span class="font-semibold text-stone-700"><i class="fa-solid fa-location-dot text-emerald-600 mr-1"></i>${p.name}</span><i class="fa-solid fa-crosshairs text-stone-400"></i></li>`).join("")
393
  : '<li class="text-stone-500">Sin parcelas aún.</li>';
394
+ renderParcelsOnMap();
395
  }
396
  function initMap() {
397
  if (map) { map.invalidateSize(); return; }
 
401
  map = L.map("map", { center: [-11.37, -77.37], zoom: 11, layers: [sat] }); // Lomas de Lachay
402
  L.control.layers({ "Satélite": sat, "Normal": osm, "Relieve": topo }).addTo(map);
403
  drawnItems = new L.FeatureGroup().addTo(map);
404
+ parcelsLayer = L.geoJSON(null, {
405
+ style: { color: "#15803d", weight: 2, fillColor: "#15803d", fillOpacity: 0.15 },
406
+ onEachFeature: (f, layer) => layer.bindPopup(f.properties.name),
407
+ }).addTo(map);
408
+ renderParcelsOnMap();
409
  const drawControl = new L.Control.Draw({
410
  draw: { polygon: true, marker: false, polyline: false, circle: false, rectangle: false, circlemarker: false },
411
  edit: { featureGroup: drawnItems, edit: false, remove: true },
 
416
  drawnGeojson = e.layer.toGeoJSON().geometry;
417
  });
418
  }
419
+ function renderParcelsOnMap() {
420
+ if (!parcelsLayer) return;
421
+ parcelsLayer.clearLayers();
422
+ const feats = parcels
423
+ .filter((p) => p.geojson)
424
+ .map((p) => ({ type: "Feature", properties: { name: p.name, id: p.id }, geometry: p.geojson }));
425
+ if (!feats.length) return;
426
+ parcelsLayer.addData({ type: "FeatureCollection", features: feats });
427
+ try { map.fitBounds(parcelsLayer.getBounds(), { maxZoom: 14, padding: [25, 25] }); } catch {}
428
+ }
429
+ function flyToParcel(id) {
430
+ const p = parcels.find((x) => x.id === id);
431
+ if (!p || !p.geojson || !map) return;
432
+ try { map.flyToBounds(L.geoJSON(p.geojson).getBounds(), { maxZoom: 15, padding: [25, 25] }); } catch {}
433
+ }
434
  $("btn-save-parcela").addEventListener("click", async () => {
435
  const name = $("parcela-name").value.trim();
436
  if (!drawnGeojson) return notify("Falta el polígono", "Dibuja un polígono en el mapa.");
 
451
  } catch (e) { notify("Error de red", String(e)); }
452
  });
453
 
454
+ // Click en una parcela de la lista -> volar a su polígono
455
+ $("parcelas-list").addEventListener("click", (e) => {
456
+ const li = e.target.closest("li[data-pid]");
457
+ if (li) flyToParcel(li.dataset.pid);
458
+ });
459
+
460
  // ===== Resumen =====
461
  async function loadResumen() {
462
  const fid = $("select-parcela").value;