// src/runtime/widget.tsx import React, { useEffect, useRef, useState } from "react"; import { AllWidgetProps } from "jimu-core"; import FeatureLayer from "@arcgis/core/layers/FeatureLayer"; import Graphic from "@arcgis/core/Graphic"; import MapView from "@arcgis/core/views/MapView"; type SlumAttrs = { objectid?: number | string; slumcode?: string; slum_cat?: string; slum_name?: string; area_name?: string; locality?: string; zone?: string; ward?: string; road_id?: string; road_name?: string; [k: string]: any; }; export default function SlumLinkWidget(props: AllWidgetProps) { const SLUM_BOUNDARY_URL = "https://gisgcc.chennaicorporation.gov.in/server/rest/services/gccdeptfield/SlumBoundary/FeatureServer/0"; const SLUM_TABLE_URL = "https://gisgcc.chennaicorporation.gov.in/server/rest/services/gccdeptfield/SlumBoundary/FeatureServer/1"; const ROAD_LAYER_URL = "https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Transporations/FeatureServer/0"; const slumBoundaryRef = useRef(null); const slumTableRef = useRef(null); const roadLayerRef = useRef(null); const viewRef = useRef(null); const clickHandlerRef = useRef(null); const [selectedSlum, setSelectedSlum] = useState(null); const [roadsInSlum, setRoadsInSlum] = useState>( [] ); const [selectedRoadIndices, setSelectedRoadIndices] = useState([]); const [status, setStatus] = useState<{ type: "idle" | "loading" | "success" | "error"; message?: string }>({ type: "idle", }); // init layers useEffect(() => { slumBoundaryRef.current = new FeatureLayer({ url: SLUM_BOUNDARY_URL }); slumTableRef.current = new FeatureLayer({ url: SLUM_TABLE_URL }); roadLayerRef.current = new FeatureLayer({ url: ROAD_LAYER_URL }); }, []); // // Helper: robustly parse applyEdits result across server versions // function parseApplyEditsResult(res: any) { if (!res) return { added: 0, updated: 0, deleted: 0, errors: [] as any[] }; const addResults = res.addFeatureResults || res.addResults || res.addFeatureResults || []; const updateResults = res.updateFeatureResults || res.updateResults || []; const deleteResults = res.deleteFeatureResults || res.deleteResults || []; const okAdded = addResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length; const okUpdated = updateResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length; const okDeleted = deleteResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length; const errors: any[] = []; for (const arr of [addResults, updateResults, deleteResults]) { for (const r of arr) { if (r && r.error) errors.push(r.error); } } return { added: okAdded, updated: okUpdated, deleted: okDeleted, errors }; } // // Helper: safe queryFeatures wrapper (returns null on error and logs the where clause) // async function safeQueryFeatures(layer: FeatureLayer | null, where: string | null, outFields: string[] = ["*"]) { if (!layer) return null; if (!where || where.trim().length === 0) { console.warn("safeQueryFeatures: skipping empty where"); return null; } const q = layer.createQuery(); q.where = where; q.outFields = outFields; q.returnGeometry = false; q.num = 2; console.debug("[safeQueryFeatures] where:", where); try { const r = await layer.queryFeatures(q); return r; } catch (err: any) { console.error("[safeQueryFeatures] queryFeatures error for where:", where, err); try { console.debug("err.details:", err?.details); console.debug("err.requestOptions:", err?.requestOptions); console.debug("err.raw:", err?.raw); } catch (e) {} return null; } } // // Helper: query SlumBoundary by geometry to get authoritative attributes // async function fetchSlumFeatureByGeometry(geometry: any): Promise { if (!slumBoundaryRef.current || !geometry) return null; try { const q = slumBoundaryRef.current.createQuery(); q.geometry = geometry; q.spatialRelationship = "intersects"; q.returnGeometry = true; q.outFields = ["*"]; q.num = 1; const res = await slumBoundaryRef.current.queryFeatures(q); if (res && res.features && res.features.length > 0) return res.features[0]; return null; } catch (err) { console.error("fetchSlumFeatureByGeometry error:", err); return null; } } // // Helper: query Slum details/table layer by slum code (layer 1) // async function fetchSlumDetailsByCode(code: string): Promise { if (!slumTableRef.current || !code) return null; try { const esc = (s: string) => (s || "").replace(/'/g, "''"); const where = `(slum_code = '${esc(code)}' OR slumcode = '${esc(code)}')`; const r = await safeQueryFeatures(slumTableRef.current, where, ["*"]); if (r && r.features && r.features.length > 0) return r.features[0]; return null; } catch (err) { console.error("fetchSlumDetailsByCode error:", err); return null; } } // Attach click -> hitTest to read slum polygon attributes and query roads useEffect(() => { const maybeView = (props as any).jimuMapView?.view || (window as any).mapView || null; if (!maybeView) return; viewRef.current = maybeView as MapView; const handler = viewRef.current.on("click", async (evt: any) => { try { setStatus({ type: "loading", message: "Identifying features..." }); const hit = await viewRef.current!.hitTest(evt); if (!hit || !hit.results || hit.results.length === 0) { setStatus({ type: "idle" }); return; } // find slum polygon result (robust detection by layer url/title containing "slum") const slumResult = hit.results.find((r: any) => { const l = r.graphic && r.graphic.layer; if (!l) return false; const u = (l.url || "").toLowerCase(); const t = (l.title || "").toLowerCase(); return u.includes("slumboundary") || t.includes("slumboundary") || t.includes("slum"); }); if (!slumResult) { setStatus({ type: "idle" }); return; } const g: any = slumResult.graphic; await handleSlumSelectedByGraphic(g); } catch (err) { console.error("Hit test error:", err); setStatus({ type: "error", message: "Identify failed" }); setTimeout(() => setStatus({ type: "idle" }), 800); } }); clickHandlerRef.current = handler; return () => { if (clickHandlerRef.current && viewRef.current) clickHandlerRef.current.remove(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [props]); // When a slum polygon graphic is selected: populate the UI fields but DO NOT save yet. async function handleSlumSelectedByGraphic(graphic: any) { try { setStatus({ type: "loading", message: "Loading slum attributes..." }); // 1) start with any attributes present on the hitTest graphic let attrs = graphic.attributes || {}; // 2) get authoritative polygon attrs if missing slum name or slum code const hasName = !!(attrs.slum_name || attrs.slumname || attrs.SLUMNAME); const hasCode = !!(attrs.slumcode || attrs.slum_code || attrs.SLUM_CODE); if ((!hasName || !hasCode) && graphic.geometry) { const featureFromLayer = await fetchSlumFeatureByGeometry(graphic.geometry); if (featureFromLayer && featureFromLayer.attributes) { // merge but prefer existing attributes from graphic if present attrs = { ...(featureFromLayer.attributes || {}), ...(attrs || {}) }; } } // Extract canonical slum code & name const slumCode = attrs.slumcode || attrs.slum_code || attrs.SLUM_CODE || ""; const slumName = attrs.slum_name || attrs.slumname || attrs.SLUMNAME || ""; const slum: SlumAttrs = { objectid: attrs.OBJECTID || attrs.objectid || attrs.objectId || attrs.ROWID, slumcode: slumCode, slum_cat: attrs.slum_cat || attrs.slumcategory || attrs.SLUMCATEGORY || "", slum_name: slumName, area_name: attrs.area_name || attrs.area || "", locality: attrs.locality || "", zone: attrs.zone || "", ward: attrs.ward || "", road_id: attrs.road_id || "", road_name: attrs.road_name || "", ...attrs, }; // 3) If we have a slum code, try to enrich from the Slum details (table) layer (layer 1) if (slumCode && slumTableRef.current) { const detailsFeature = await fetchSlumDetailsByCode(slumCode); if (detailsFeature && detailsFeature.attributes) { const d = detailsFeature.attributes; // merge relevant fields (prefer the existing slum object values) slum.slum_name = slum.slum_name || d.slum_name || d.slumname || ""; slum.area_name = slum.area_name || d.area_name || d.area || ""; slum.zone = slum.zone || d.zone || ""; slum.ward = slum.ward || d.ward || ""; // merge any other useful attributes you want: // slum.household = slum.household || d.household; // slum.no_ofstreets = slum.no_ofstreets || d.no_ofstreets; slum["__slum_table_oid"] = d.OBJECTID || d.objectid || d.objectId; } } // Set UI state so user can edit before saving setSelectedSlum(slum); setSelectedRoadIndices([]); setRoadsInSlum([]); // Query roads intersecting polygon (use geometry from the graphic) if (graphic.geometry && roadLayerRef.current) { setStatus({ type: "loading", message: "Querying roads..." }); const q = roadLayerRef.current.createQuery(); q.geometry = graphic.geometry; q.spatialRelationship = "intersects"; q.returnGeometry = false; q.outFields = ["Road_ID", "Road_Name", "zone", "ward", "*"]; q.num = 1000; const res = await roadLayerRef.current.queryFeatures(q); const matches: Array<{ road_id?: string; road_name?: string; attrs?: any }> = []; if (res && res.features && res.features.length > 0) { const seen = new Set(); for (const f of res.features) { const rattrs = f.attributes || {}; const rid = rattrs.Road_ID || rattrs.road_id || rattrs.RoadId || rattrs.ROAD_ID || ""; const rname = rattrs.Road_Name || rattrs.road_name || rattrs.RoadName || rattrs.Road || ""; const key = `${String(rid).trim()}__${String(rname).trim()}`; if (!seen.has(key)) { seen.add(key); matches.push({ road_id: String(rid || "").trim(), road_name: String(rname || "").trim(), attrs: rattrs }); } } } matches.sort((a, b) => (a.road_name || "").localeCompare(b.road_name || "")); setRoadsInSlum(matches); } setStatus({ type: "success", message: "Slum loaded — review and save if needed" }); } catch (err) { console.error("handleSlumSelectedByGraphic:", err); setStatus({ type: "error", message: "Failed to load slum" }); } finally { setTimeout(() => setStatus({ type: "idle" }), 900); } } // // When user selects a single road (we keep behavior: selecting one will immediately upsert that single record) // async function handleRoadPickSingle(idx: number) { if (idx === -1) { setSelectedRoadIndices([]); return; } const road = roadsInSlum[idx]; if (!road || !selectedSlum) return; // Merge authoritative road attrs into slum const updated: SlumAttrs = { ...(selectedSlum || {}) }; updated.road_id = road.road_id || road.attrs?.Road_ID || road.attrs?.road_id || updated.road_id; updated.road_name = road.road_name || road.attrs?.Road_Name || road.attrs?.road_name || updated.road_name; const rattrs = road.attrs || {}; updated.zone = rattrs.zone || rattrs.Zone || updated.zone; updated.ward = rattrs.ward || rattrs.Ward || updated.ward; setSelectedSlum(updated); setSelectedRoadIndices([idx]); // upsert single await upsertSlumTable(updated); } // // multi-select handler: if exactly one selected, we call single upsert to preserve previous behaviour // function handleRoadsMultiSelect(e: React.ChangeEvent) { const options = e.target.options; const selected: number[] = []; for (let i = 0; i < options.length; i++) { if (options[i].selected) { const val = Number(options[i].value); if (!isNaN(val) && val >= 0) selected.push(val); } } setSelectedRoadIndices(selected); // if single selection made, keep previous behavior: immediately upsert that one if (selected.length === 1) { handleRoadPickSingle(selected[0]); } } // // Bulk save: create/update one row per selected road (uses safeQueryFeatures and robust parse) // async function saveSelectedRoadsBulk() { if (!selectedSlum) { alert("Load and/or enter slum attributes first by clicking a slum polygon."); return; } if (!slumTableRef.current) { alert("Slum table layer not ready."); return; } if (!selectedRoadIndices || selectedRoadIndices.length === 0) { alert("No roads selected."); return; } try { setStatus({ type: "loading", message: "Preparing bulk save..." }); const addFeatures: any[] = []; const updateFeatures: any[] = []; const esc = (s: string) => (s || "").replace(/'/g, "''"); for (const idx of selectedRoadIndices) { const road = roadsInSlum[idx]; if (!road) continue; const candidate: any = { slum_code: selectedSlum.slumcode || "", slumcategory: selectedSlum.slum_cat || "", slumname: selectedSlum.slum_name || "", area_name: selectedSlum.area_name || selectedSlum.locality || "", zone: selectedSlum.zone || "", ward: selectedSlum.ward || "", road_id: road.road_id || road.attrs?.Road_ID || road.attrs?.road_id || "", road_name: road.road_name || road.attrs?.Road_Name || road.attrs?.road_name || "", }; if (road.attrs) { candidate.zone = road.attrs.zone || road.attrs.Zone || candidate.zone; candidate.ward = road.attrs.ward || road.attrs.Ward || candidate.ward; } let foundFeature: any = null; // primary match: slum_code + road_id if (candidate.slum_code && candidate.road_id) { const where = `(slum_code = '${esc(candidate.slum_code)}' OR slumcode = '${esc(candidate.slum_code)}') AND (road_id = '${esc( candidate.road_id )}')`; const r = await safeQueryFeatures(slumTableRef.current, where, ["*"]); if (r && r.features && r.features.length > 0) foundFeature = r.features[0]; } // fallback: slumname + road_name (case-insensitive) if (!foundFeature && candidate.slumname && candidate.road_name) { const where = `(UPPER(slumname) = UPPER('${esc(candidate.slumname)}') OR UPPER(slum_name) = UPPER('${esc(candidate.slumname)}'))` + ` AND (UPPER(road_name) = UPPER('${esc(candidate.road_name)}') OR UPPER(roadname) = UPPER('${esc(candidate.road_name)}'))`; const r2 = await safeQueryFeatures(slumTableRef.current, where, ["*"]); if (r2 && r2.features && r2.features.length > 0) foundFeature = r2.features[0]; } // attributes to apply const attributesToApply: any = {}; if (candidate.slum_code) attributesToApply["slum_code"] = candidate.slum_code; if (candidate.slumcategory) attributesToApply["slumcategory"] = candidate.slumcategory; if (candidate.slumname) attributesToApply["slumname"] = candidate.slumname; if (candidate.area_name) attributesToApply["area_name"] = candidate.area_name; if (candidate.zone) attributesToApply["zone"] = candidate.zone; if (candidate.ward) attributesToApply["ward"] = candidate.ward; if (candidate.road_id) attributesToApply["road_id"] = candidate.road_id; if (candidate.road_name) attributesToApply["road_name"] = candidate.road_name; if (foundFeature) { const oidName = (foundFeature.attributes && Object.keys(foundFeature.attributes).find((k) => /^(objectid|oid|object_id|gid)$/i.test(k))) || "OBJECTID"; attributesToApply[oidName] = foundFeature.attributes[oidName]; updateFeatures.push(new Graphic({ attributes: attributesToApply })); } else { addFeatures.push(new Graphic({ attributes: attributesToApply })); } } // end for // apply edits setStatus({ type: "loading", message: `Saving ${addFeatures.length} adds & ${updateFeatures.length} updates...` }); const applyResult = await slumTableRef.current.applyEdits({ addFeatures, updateFeatures }); console.debug("applyEdits raw:", applyResult); const parsed = parseApplyEditsResult(applyResult); if ((addFeatures.length === 0 || parsed.added === addFeatures.length) && (updateFeatures.length === 0 || parsed.updated === updateFeatures.length)) { setStatus({ type: "success", message: `Bulk save OK: ${parsed.added} added, ${parsed.updated} updated` }); } else { console.warn("applyEdits returned (partial/failed):", applyResult, parsed.errors); setStatus({ type: "error", message: "Bulk save partially failed (see console)" }); } } catch (err) { console.error("bulk save error:", err); setStatus({ type: "error", message: "Bulk save error" }); } finally { setTimeout(() => setStatus({ type: "idle" }), 1000); } } // // Upsert single slum-row (tries match by slum_code then slumname) // async function upsertSlumTable(slum: SlumAttrs) { if (!slumTableRef.current) { console.warn("slum table not ready"); return; } try { setStatus({ type: "loading", message: "Saving slum table..." }); const esc = (s: string) => (s || "").replace(/'/g, "''"); const code = slum.slumcode || slum.slum_code || ""; const name = slum.slum_name || slum.slumname || ""; let foundFeature: any = null; // Try match by slum_code if (code) { const where = `slum_code = '${esc(String(code))}' OR slumcode = '${esc(String(code))}'`; const r = await safeQueryFeatures(slumTableRef.current, where, ["*"]); if (r && r.features && r.features.length > 0) foundFeature = r.features[0]; } // fallback to name match (case-insensitive) if (!foundFeature && name) { const where = `UPPER(slumname) = UPPER('${esc(String(name))}') OR UPPER(slum_name) = UPPER('${esc(String(name))}')`; const r2 = await safeQueryFeatures(slumTableRef.current, where, ["*"]); if (r2 && r2.features && r2.features.length > 0) foundFeature = r2.features[0]; } // Build attributes map to update/add const updateAttrs: any = {}; if (slum.slumcode) updateAttrs["slum_code"] = slum.slumcode; if (slum.slum_cat) updateAttrs["slumcategory"] = slum.slum_cat; if (slum.slum_name) updateAttrs["slumname"] = slum.slum_name; if (slum.zone) updateAttrs["zone"] = slum.zone; if (slum.ward) updateAttrs["ward"] = slum.ward; if (slum.road_id) updateAttrs["road_id"] = slum.road_id; if (slum.road_name) updateAttrs["road_name"] = slum.road_name; if (slum.area_name) updateAttrs["area_name"] = slum.area_name; if (slum.locality) updateAttrs["locality"] = slum.locality; let applyResult: any = null; if (foundFeature) { const oidName = (foundFeature.attributes && Object.keys(foundFeature.attributes).find((k) => /^(objectid|oid|object_id|gid)$/i.test(k))) || "OBJECTID"; updateAttrs[oidName] = foundFeature.attributes[oidName]; applyResult = await slumTableRef.current.applyEdits({ updateFeatures: [new Graphic({ attributes: updateAttrs })] }); } else { applyResult = await slumTableRef.current.applyEdits({ addFeatures: [new Graphic({ attributes: updateAttrs })] }); } console.debug("applyEdits raw (upsert):", applyResult); const parsed = parseApplyEditsResult(applyResult); if (parsed.added > 0 || parsed.updated > 0) { setStatus({ type: "success", message: "Slum table updated" }); } else { console.warn("applyEdits returned (upsert):", applyResult, parsed.errors); setStatus({ type: "error", message: "Save failed (see console)" }); } } catch (err) { console.error("upsertSlumTable error:", err); setStatus({ type: "error", message: "Save error" }); } finally { setTimeout(() => setStatus({ type: "idle" }), 900); } } // Manual Save (in case user edits fields and wants to explicitly save without picking road) async function handleManualSave() { if (!selectedSlum) { alert("No slum data to save."); return; } await upsertSlumTable(selectedSlum); } // UI return (

Slum → Table (click polygon to load)

Status:{" "} {status.type === "idle" ? "idle" : status.type === "loading" ? `working — ${status.message || ""}` : status.message}
setSelectedSlum((s) => ({ ...(s || {}), slumcode: e.target.value }))} style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }} />
setSelectedSlum((s) => ({ ...(s || {}), slum_name: e.target.value }))} style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }} />
setSelectedSlum((s) => ({ ...(s || {}), slum_cat: e.target.value }))} style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }} />
setSelectedSlum((s) => ({ ...(s || {}), area_name: e.target.value }))} style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }} />
setSelectedSlum((s) => ({ ...(s || {}), zone: e.target.value }))} style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }} />
setSelectedSlum((s) => ({ ...(s || {}), ward: e.target.value }))} style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }} />
1 ? `${selectedRoadIndices.length} selected` : selectedSlum?.road_id || "" } readOnly style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }} />
1 ? `${selectedRoadIndices.length} selected` : selectedSlum?.road_name || "" } readOnly style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }} />
Click a slum polygon to load its attributes into the fields (you can edit them). You can multi-select roads (Ctrl/Cmd+click). Selecting a single road will immediately merge road fields and save that row. Use Save Selected Roads (bulk) to create/update one row per selected road in the Slum Table.
); }