Spaces:
Sleeping
Sleeping
| import Map from "https://js.arcgis.com/4.33/@arcgis/core/Map.js"; | |
| import MapView from "https://js.arcgis.com/4.33/@arcgis/core/views/MapView.js"; | |
| import * as reactiveUtils from "https://js.arcgis.com/4.33/@arcgis/core/core/reactiveUtils.js"; | |
| import GeoJSONLayer from "https://js.arcgis.com/4.33/@arcgis/core/layers/GeoJSONLayer.js"; | |
| import GraphicsLayer from "https://js.arcgis.com/4.33/@arcgis/core/layers/GraphicsLayer.js"; | |
| import Graphic from "https://js.arcgis.com/4.33/@arcgis/core/Graphic.js"; | |
| import Polygon from "https://js.arcgis.com/4.33/@arcgis/core/geometry/Polygon.js"; | |
| import Swipe from "https://js.arcgis.com/4.33/@arcgis/core/widgets/Swipe.js"; | |
| import Legend from "https://js.arcgis.com/4.33/@arcgis/core/widgets/Legend.js"; | |
| import Home from "https://js.arcgis.com/4.33/@arcgis/core/widgets/Home.js"; | |
| import Expand from "https://js.arcgis.com/4.33/@arcgis/core/widgets/Expand.js"; | |
| import * as projection from "https://js.arcgis.com/4.33/@arcgis/core/geometry/projection.js"; | |
| // ?? Leer par metros | |
| const params = new URLSearchParams(window.location.search); | |
| const before_url = params.get("before"); | |
| const after_url = params.get("after"); | |
| const change_url = params.get("change"); | |
| const split_url = params.get("split"); | |
| const temp_change = params.get("slider_value"); | |
| const sim_type = params.get("mode"); | |
| console.log("mode", sim_type) | |
| console.log("temp_change", temp_change) | |
| console.log("before_url", before_url) | |
| const STORAGE_KEY = "linked_map_selected_zona"; | |
| function getStoredZonaId() { | |
| const raw = window.localStorage.getItem(STORAGE_KEY); | |
| if (!raw) return null; | |
| const parsed = Number(raw); | |
| return Number.isFinite(parsed) ? parsed : null; | |
| } | |
| function setStoredZonaId(zonaId) { | |
| console.log("setStoredZonaId", zonaId) | |
| if (zonaId == null) return; | |
| window.localStorage.setItem(STORAGE_KEY, String(zonaId)); | |
| } | |
| // ??? Capas base | |
| const zonasLayer = new GeoJSONLayer({ | |
| id: "zonasLayer", | |
| url: "layers/Zonas.geojson", | |
| outFields: ["*"], | |
| opacity: 0.6, | |
| renderer: { | |
| type: "simple", | |
| symbol: { | |
| type: "simple-fill", | |
| color: [127, 154, 99, 0.6], | |
| outline: { | |
| color: [155, 255, 0, 1], | |
| width: 2 | |
| } | |
| } | |
| } | |
| }); | |
| const beforeLayer = new GeoJSONLayer({ url: before_url, opacity: 0 }); | |
| const afterLayer = new GeoJSONLayer({ url: after_url, opacity: 0 }); | |
| const changeLayer = new GeoJSONLayer({ url: change_url, opacity: 0 }); | |
| let splitGeoLayer = null; | |
| if (split_url) | |
| splitGeoLayer = new GeoJSONLayer({ url: split_url }); | |
| // ?? Layers gr ficos | |
| const beforeBufferLayer = new GraphicsLayer({ title: "Antes (cuadrados)" }); | |
| const beforeBufferLayer2 = new GraphicsLayer({ title: "Antes (cuadrados)" }); | |
| const afterBufferLayer = new GraphicsLayer({ title: "Después (cuadrados)" }); | |
| const changeBufferLayer = new GraphicsLayer({ title: "Cambio (cuadrados)" }); | |
| await zonasLayer.load(); | |
| await beforeLayer.load(); | |
| // Obtén su extent | |
| const extent = zonasLayer.fullExtent; | |
| // Calcula el punto central del extent | |
| const center = extent.center; | |
| // ?? Mapa compartido (solo before/after) | |
| const map = new Map({ | |
| basemap: "satellite", | |
| layers: [ zonasLayer,beforeBufferLayer2,] | |
| }); | |
| const map2 = new Map({ | |
| basemap: "satellite", | |
| layers: [beforeBufferLayer, afterBufferLayer] | |
| }); | |
| const homeViewpoint = { | |
| targetGeometry: center, | |
| scale: 50000 | |
| }; | |
| // ?? View 1 (izquierda) | |
| const view1 = new MapView({ | |
| container: "view1Div", | |
| map, | |
| center: [-90.493862, 14.61925], | |
| zoom: 11, | |
| constraints: { snapToZoom: false }, | |
| viewpoint: homeViewpoint | |
| }); | |
| // ?? View 2 (derecha) | |
| const view2 = new MapView({ | |
| container: "view2Div", | |
| map: map2, | |
| center: [-90.493862, 14.61925], | |
| zoom: 11, | |
| constraints: { snapToZoom: false }, | |
| viewpoint: homeViewpoint | |
| }); | |
| const modeControls = document.getElementById("modeControls"); | |
| // Crear widget Expand con ese contenido | |
| const expandWidget = new Expand({ | |
| view: view2, | |
| content: modeControls, | |
| collapseIcon: "sliders", | |
| expandIcon: "sliders", | |
| expanded: true, | |
| expandTooltip: "Modo de visualizaci¢n", | |
| mode: "floating" | |
| }); | |
| const temperature = document.getElementById("temperature"); | |
| const tempWidget = new Expand({ | |
| view: view2, | |
| content: temperature, | |
| collapseIcon: "sliders", | |
| expandIcon: "spike-data-visualization-outlined-gradient", | |
| expanded: false, | |
| expandTooltip: "Modo de visualizaci¢n", | |
| mode: "floating" | |
| }); | |
| view2.ui.add(expandWidget, "top-right"); | |
| view2.ui.add(tempWidget, "bottom-right"); | |
| const labelEstimated = document.getElementById("labelEstimated"); | |
| const labelChange = document.getElementById("labelChange"); | |
| const card2 = document.createElement("calcite-card"); | |
| const title2 = document.createElement("span"); | |
| title2.slot = "title"; | |
| const card = document.createElement("calcite-card"); | |
| const title = document.createElement("span"); | |
| title.slot = "title"; | |
| title.textContent = `${sim_type} actual`; | |
| function updateLabels() { | |
| console.log("updateLabels", sim_type); | |
| if (!labelEstimated || !labelChange) return; | |
| console.log("entro?"); | |
| title.textContent = `${sim_type} actual`; | |
| if (sim_type === "Cobertura") { | |
| labelEstimated.textContent = "Cobertura estimada"; | |
| labelChange.textContent = "Cambio de cobertura"; | |
| title2.textContent = `Cobertura estimada después de la disminución\nde la temperatura ${temp_change}`; | |
| } else { | |
| labelEstimated.textContent = "Temperatura estimada"; | |
| labelChange.textContent = "Disminución de temperatura"; | |
| title2.textContent = `Temperatura estimada después del aumento\nde cobertura arbórea ${temp_change}`; | |
| } | |
| } | |
| updateLabels(); | |
| card2.appendChild(title2); | |
| card.appendChild(title); | |
| view1.ui.add(card, "bottom-left"); | |
| view2.ui.add(card2, "bottom-left"); | |
| view2.ui.move("zoom", "top-left"); | |
| view1.ui.move("zoom", "top-left"); | |
| // ? Capa "Cambio" solo en view2 | |
| view2.map.add(changeBufferLayer); | |
| changeBufferLayer.visible = false; | |
| // ? Capa "Split" solo en view2 | |
| const splitGraphicsLayer = new GraphicsLayer({ title: "Split Graphics" }); | |
| view2.map.add(splitGraphicsLayer); | |
| splitGraphicsLayer.visible = false; | |
| // ?? Sincronizaci¢n entre vistas | |
| const views = [view1, view2]; | |
| let active; | |
| function sync(source) { | |
| if (!active || !active.viewpoint || active !== source) return; | |
| for (const view of views) { | |
| if (view !== active) view.viewpoint = active.viewpoint.clone(); | |
| } | |
| } | |
| for (const view of views) { | |
| reactiveUtils.watch(() => [view.interacting, view.viewpoint], ([interacting, viewpoint]) => { | |
| if (interacting) { | |
| active = view; | |
| sync(active); | |
| } | |
| if (viewpoint) { | |
| sync(view); | |
| } | |
| }); | |
| } | |
| async function applyZonaSelection(zonaFeature, { persist = true } = {}) { | |
| const zonaGeom = zonaFeature?.geometry; | |
| console.log("zonaGeom", zonaGeom); | |
| if (!zonaGeom) return; | |
| if (persist) { | |
| const zonaId = zonaFeature?.attributes?.properties?.zona ?? zonaFeature?.attributes?.zona; | |
| setStoredZonaId(zonaId); | |
| } | |
| const q = beforeLayer.createQuery(); | |
| q.geometry = zonaGeom; | |
| q.spatialRelationship = "intersects"; // o "contains" | |
| q.outFields = ["*"]; | |
| q.returnGeometry = true; | |
| console.log("q", q); | |
| const result = await beforeLayer.queryFeatures(q); | |
| const resultAfter = await afterLayer.queryFeatures(q); | |
| const resultChange = await changeLayer.queryFeatures(q); | |
| console.log("result", result.features.length); | |
| let targetExtent = result?.extent || zonaGeom?.extent; | |
| if (targetExtent) { | |
| const expanded = targetExtent.clone().expand(1.3); | |
| // Mueve ambos mapas al mismo tiempo | |
| await Promise.allSettled([ | |
| view1.goTo(expanded, { animate: true, duration: 700 }), | |
| view2.goTo(expanded, { animate: true, duration: 700 }) | |
| ]); | |
| } | |
| beforeBufferLayer.removeAll(); | |
| beforeBufferLayer2.removeAll(); | |
| afterBufferLayer.removeAll(); | |
| changeBufferLayer.removeAll(); | |
| await createSquaresFromFeatures(result.features, beforeBufferLayer2); | |
| await createSquaresFromFeatures(result.features, beforeBufferLayer); | |
| await createSquaresFromFeatures(resultAfter.features, afterBufferLayer); | |
| await createSquaresFromFeatures(resultChange.features, changeBufferLayer); | |
| if (splitGeoLayer) { | |
| const resultSplit = await splitGeoLayer.queryFeatures(q); | |
| splitGraphicsLayer.removeAll(); | |
| await createSquaresFromFeatures(resultSplit.features, splitGraphicsLayer, false); | |
| } | |
| } | |
| async function restoreZonaSelection() { | |
| const zonaId = getStoredZonaId(); | |
| if (zonaId == null) return; | |
| const q = zonasLayer.createQuery(); | |
| q.where = `zona = ${zonaId}`; | |
| q.outFields = ["*"]; | |
| q.returnGeometry = true; | |
| const result = await zonasLayer.queryFeatures(q); | |
| const zonaFeature = result.features[0]; | |
| if (!zonaFeature) return; | |
| await applyZonaSelection(zonaFeature, { persist: false }); | |
| } | |
| view1.on("click", async (event) => { | |
| const hit = await view1.hitTest(event); | |
| console.log("click", hit.results) | |
| const zonaHit = hit.results.find(r => r.graphic?.layer?.id === zonasLayer.id); | |
| if (!zonaHit) return; | |
| console.log("zona hit ", zonaHit) | |
| console.log("attributes", zonaHit.graphic.attributes) | |
| const zonaGraphic = zonaHit.graphic; | |
| await applyZonaSelection(zonaGraphic); | |
| }) | |
| let hoverGraphic = null; | |
| view1.on("pointer-move", async (event) => { | |
| const hit = await view1.hitTest(event); | |
| const zonaHit = hit.results.find( | |
| r => r.graphic?.layer?.id === zonasLayer.id | |
| ); | |
| // quitar hover anterior | |
| if (hoverGraphic) { | |
| view1.graphics.remove(hoverGraphic); | |
| hoverGraphic = null; | |
| } | |
| if (!zonaHit) return; | |
| hoverGraphic = zonaHit.graphic.clone(); | |
| hoverGraphic.symbol = { | |
| type: "simple-fill", | |
| color: [127, 154, 99, 0.6], | |
| outline: { | |
| color: [155, 255, 0, 1], | |
| width: 2.5 | |
| } | |
| }; | |
| view1.graphics.add(hoverGraphic); | |
| }); | |
| async function createSquaresFromFeatures(features, targetGraphicsLayer, useBuffer = true) { | |
| await projection.load(); | |
| const half = 50; // metros en UTM | |
| const graphics = []; | |
| for (const f of features) { | |
| const attrs = f.attributes; | |
| const geom = f.geometry; | |
| if (!geom) continue; | |
| const colorHex = attrs.color ?? "#999"; | |
| const colorRGB = hexToRGB(colorHex, 0.9); | |
| if (useBuffer){ | |
| const utm = projection.project(geom, { wkid: 32615 }); | |
| if (!utm) continue; | |
| const { x, y } = utm; | |
| const squareUTM = new Polygon({ | |
| spatialReference: { wkid: 32615 }, | |
| rings: [[ | |
| [x - half, y - half], | |
| [x + half, y - half], | |
| [x + half, y + half], | |
| [x - half, y + half], | |
| [x - half, y - half], | |
| ]], | |
| }); | |
| const squareWGS = projection.project(squareUTM, { wkid: 4326 }); | |
| graphics.push(new Graphic({ | |
| geometry: squareWGS, | |
| symbol: { | |
| type: "simple-fill", | |
| color: colorRGB, | |
| outline: { color: [40, 40, 40, 0.2], width: 0 }, | |
| }, | |
| })); | |
| } else { | |
| graphics.push( | |
| new Graphic({ | |
| geometry: geom, | |
| attributes: attrs, | |
| symbol: { | |
| type: "simple-marker", | |
| path: "M -1 -1 L 1 1 M -1 1 L 1 -1", | |
| color: colorRGB, | |
| size: 9, | |
| outline: { | |
| color: colorRGB, | |
| width: 6, | |
| }, | |
| }, | |
| }) | |
| ); | |
| } | |
| } | |
| targetGraphicsLayer.addMany(graphics); | |
| } | |
| // ?? Crear buffers | |
| async function createSquaresFromLayer(geoLayer, targetGraphicsLayer, valueField = "value") { | |
| await projection.load(); | |
| await geoLayer.load(); | |
| const features = await geoLayer.queryFeatures(); | |
| const half = 50; | |
| const graphics = []; | |
| for (const f of features.features) { | |
| const attrs = f.attributes; | |
| const geom = f.geometry; | |
| const colorHex = attrs.color ?? "#999"; | |
| const colorRGB = hexToRGB(colorHex, 0.9); | |
| const utm = projection.project(geom, { wkid: 32615 }); | |
| if (!utm) continue; | |
| const { x, y } = utm; | |
| const squareUTM = new Polygon({ | |
| spatialReference: { wkid: 32615 }, | |
| rings: [ | |
| [ | |
| [x - half, y - half], | |
| [x + half, y - half], | |
| [x + half, y + half], | |
| [x - half, y + half], | |
| [x - half, y - half], | |
| ], | |
| ], | |
| }); | |
| const squareWGS = projection.project(squareUTM, { wkid: 4326 }); | |
| graphics.push(new Graphic({ | |
| geometry: squareWGS, | |
| symbol: { | |
| type: "simple-fill", | |
| color: colorRGB, | |
| outline: { color: [40, 40, 40, 0.2], width: 0 }, | |
| }, | |
| })); | |
| } | |
| targetGraphicsLayer.addMany(graphics); | |
| } | |
| function hexToRGB(hex, alpha = 1) { | |
| const bigint = parseInt(hex.replace("#", ""), 16); | |
| return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255, alpha]; | |
| } | |
| // ?? Render buffers | |
| // await createSquaresFromLayer(beforeLayer, beforeBufferLayer); | |
| // await createSquaresFromLayer(beforeLayer, beforeBufferLayer2); | |
| // await createSquaresFromLayer(afterLayer, afterBufferLayer); | |
| // await createSquaresFromLayer(changeLayer, changeBufferLayer); | |
| // ?? Swipe en view2 | |
| const swipe = new Swipe({ | |
| view: view2, | |
| trailingLayers: [beforeBufferLayer], | |
| leadingLayers: [afterBufferLayer, changeBufferLayer], | |
| position: 50, | |
| direction: "vertical", | |
| }); | |
| view2.ui.add(swipe); | |
| // ?? Leyenda en view2 | |
| const legend = new Legend({ | |
| view: view2, | |
| layerInfos: [ | |
| { layer: beforeBufferLayer, title: "Antes" }, | |
| { layer: afterBufferLayer, title: "Despu‚s" }, | |
| { layer: changeBufferLayer, title: "Cambio" }, | |
| { layer: splitGraphicsLayer, title: "Split" }, | |
| ], | |
| }); | |
| //view2.ui.add(legend, "bottom-left"); | |
| // ??? Radio buttons - controlan solo view2 | |
| const home = new Home({ view: view1 }); | |
| const home2 = new Home({ view: view2 }); | |
| view1.ui.add(home, "top-left"); | |
| view2.ui.add(home2, "top-left"); | |
| // ?? Interceptar cuando el usuario presiona Home | |
| home.go = async function () { | |
| console.log("entro") | |
| // Llama a la animaci¢n normal de la vista principal | |
| //await view1.goTo(homeViewpoint, { animate: true }); | |
| await view1.goTo(zonasLayer.fullExtent, { animate: true }); | |
| console.log("zonaslayer2", zonasLayer) | |
| // Sincroniza la otra vista tambi‚n | |
| //await view2.goTo(homeViewpoint, { animate: true }); | |
| }; | |
| await restoreZonaSelection(); | |
| const vm = document.getElementById("viewModeGroup"); | |
| vm.selectedItem = "swipe" | |
| const img = document.querySelector("#modeImage"); | |
| vm.addEventListener("calciteRadioButtonGroupChange", (event) => { | |
| const selected = event.target.selectedItem; | |
| const mode = selected?.value; | |
| console.log("?? Cambio detectado:", mode); | |
| if (mode === "swipe") { | |
| //swipe.visible = true; | |
| beforeBufferLayer.visible = true; | |
| afterBufferLayer.visible = true; | |
| changeBufferLayer.visible = false; | |
| img.src = "data/before_after_colorbar_horizontal.png"; | |
| img.alt = "Vista Swipe"; | |
| title2.textContent = `Temperatura estimada después del aumento\nde cobertura arbórea ${temp_change}` | |
| } else if (mode === "change") { | |
| //swipe.visible = false; | |
| //beforeBufferLayer.visible = false; | |
| afterBufferLayer.visible = false; | |
| changeBufferLayer.visible = true; | |
| img.src = "data/change_colorbar_horizontal.png"; | |
| img.alt = "Vista de cambio"; | |
| title2.textContent = `Disminución de la temperatura después del aumento\nde cobertura arbórea ${temp_change}` | |
| } | |
| }); | |
| const sw = document.getElementById("filtroSwitch"); | |
| sw.addEventListener("calciteSwitchChange", (event) => { | |
| const isOn = event.target.checked; | |
| console.log("Switch cambi¢:", isOn); | |
| if (isOn) { | |
| console.log("?? Activar capa de rboles"); | |
| splitGraphicsLayer.visible = true; | |
| } else { | |
| console.log("?? Desactivar capa de rboles"); | |
| splitGraphicsLayer.visible = false; | |
| } | |
| }); | |
| window.addEventListener("message", async (event) => { | |
| const msg = event.data; | |
| //console.log("Mensaje recibido en linked_map.js:", msg); | |
| if (msg?.type === "setMode") { | |
| sim_type = msg.mode; | |
| temp_change = msg.value; | |
| updateLabels(); | |
| } | |
| }); |