let COLORS = { B: "#bbb", L: "blue", R: "orange", C: "teal", }; let activeZone = ""; let baseAttributes = { opacity: 0.8, fillOpacity: 0.2, weight: 1, }; let selectedAttributes = { opacity: 0.8, fillOpacity: 0.4, weight: 5, }; App.resetActive = () => { history.replaceState("", null, "/"); App.clearActiveZone(); App.map.setZoom(12); }; App.getZoneStyle = feature => { if (feature.properties.state == "B") { return { color: COLORS[feature.properties.state], opacity: 1, fillOpacity: 0, weight: 3, className: `zone-${feature.id}`, }; } return { color: COLORS[feature.properties.state], className: `zone-${feature.id}`, ...baseAttributes, }; }; App.renderTooltip = feature => { let props = feature.properties; let seedId = props.seed; let seedHtml = ""; if (seedId) { const seed = App.seeds[seedId.toString()]; seedHtml = `

${seed.name}

`; } let custodianHtml = props.custodian_name ? `

${props.custodian_name}

` : ""; return `

${props.name}

${ props.state_str }

${seedHtml} ${custodianHtml}
`; }; App.getCenter = feature => { const coords = feature.geometry.coordinates[0]; const xs = coords.map(c => c[0]); const ys = coords.map(c => c[1]); const minx = Math.min(...xs); const maxx = Math.max(...xs); const miny = Math.min(...ys); const maxy = Math.max(...ys); return [(miny + maxy) / 2, (minx + maxx) / 2]; }; App.clickedZone = feature => { document .getElementById("map") .dispatchEvent(new CustomEvent("map:click", { detail: feature })); }; App.goToZoneFromHash = () => { let chosenZone = document.location.hash.replace("#", ""); if (chosenZone == "") { App.clearActiveZone(); } else { App.clickedZone({ id: parseInt(chosenZone) }); } }; App.focusOnZone = id => { App.clearActiveZone(); activeZone = `zone-${id}`; let elem = document.getElementsByClassName(activeZone)[0]; elem.setAttribute("fill-opacity", selectedAttributes.fillOpacity); elem.setAttribute("stroke-width", selectedAttributes.weight); setTimeout(() => elem.focus(), 2); let feature = App.findFeature(id); App.map.setView(App.getCenter(feature), 15, { animate: true, }); }; App.clearActiveZone = () => { try { let elem = document.getElementsByClassName(activeZone)[0]; elem.setAttribute("fill-opacity", baseAttributes.fillOpacity); elem.setAttribute("stroke-width", baseAttributes.weight); } catch (e) { // ignore } activeZone = ""; }; App.findFeature = id => { return App.features.find(f => f.id === id); }; App.initMap = async (map, options) => { // Global variable App defined in map.html App.map = map; let spinner = document.getElementById("spinner"); spinner.classList.add("htmx-request"); // Download GeoJSON via Ajax let resp = await fetch(App.geoDataUrl); let data = await resp.json(); App.features = data.features; // Download seed data resp = await fetch(App.seedsUrl); App.seeds = await resp.json(); spinner.classList.remove("htmx-request"); // https://github.com/brunob/leaflet.fullscreen if (L.control.fullscreen) { L.control .fullscreen({ position: "topleft", // change the position of the button can be topleft, topright, bottomright or bottomleft, default topleft title: "Pantalla completa", // change the title of the button, default Full Screen titleCancel: "Surt de pantalla completa", // change the title of the button when fullscreen is on, default Exit Full Screen // content: null, // change the content of the button, can be HTML, default null forceSeparateButton: true, // force separate button to detach from zoom buttons, default false forcePseudoFullscreen: true, // force use of pseudo full screen even if full screen API is available, default false fullscreenElement: false, // Dom element to render in full screen, false by default, fallback to map._container }) .addTo(map); } L.geoJson(data, { // for the base layer // interactive: false, onEachFeature: (feature, layer) => { feature.layer = layer; // skip "base" features, those are non-interactive, e.g. perimeter if (feature.properties.state !== "B") { layer.bindTooltip(App.renderTooltip(feature), { direction: "top", offset: L.point(0, -4), opacity: 1, className: "z-10", }); layer.on({ click: () => { document.location.hash = feature.id.toString(); }, }); } else { layer.interactive = false; } }, style: App.getZoneStyle, }).addTo(map); var legend = L.control({ position: "topright" }); legend.onAdd = function (map) { let div = L.DomUtil.create("div", "info legend"), elements = [ ["L", "Desprotegida"], ["R", "Reservada"], ["C", "Custodiada"], ]; for (var [elem, label] of elements) { div.innerHTML += `

${label}

`; } return div; }; legend.addTo(map); // Interactivity let mapElem = document.getElementById("map"); mapElem.addEventListener("map:click", async e => { App.focusOnZone(e.detail.id); // WARNING: hardcoded as we would need to reverse from JS let url = `/zones/${e.detail.id}?x=true`; let resp = await htmx.ajax("GET", url, { target: "#sidebar", swap: "innerHTML", }); _hyperscript.browserInit(); document.getElementById("sidebar-close").classList.remove("hidden"); }); mapElem.addEventListener("zone:refresh", e => { let feature = App.findFeature(e.detail.id); console.log("Handle zone:refresh", { detail: e.detail, feature }); if (e.detail.state) { feature.properties.custodian_name = e.detail.custodian_name; feature.properties.state = e.detail.state; feature.properties.state_str = e.detail.state_str; } // update map element in case zone state has changed // (after form submission) let style = App.getZoneStyle(feature); if (e.detail.autofocus) { style.fillOpacity = selectedAttributes.fillOpacity; style.weight = selectedAttributes.weight; } feature.layer.setStyle(style); feature.layer.setTooltipContent(App.renderTooltip(feature)); }); App.refreshedZone = (zone, autofocus = true) => { document .getElementById("map") .dispatchEvent( new CustomEvent("zone:refresh", { detail: { ...zone, autofocus } }) ); }; window.addEventListener("hashchange", event => { App.goToZoneFromHash(); }); // upon page load, honor hash in URL to be zone id App.goToZoneFromHash(); };