### Initial UI Setup for Hex Picker (JavaScript) Source: https://github.com/alti3/hexpicker/blob/master/index.html This section contains the initial setup calls for the hex picker's user interface. It ensures that copy buttons are updated and the list of zones is rendered when the application loads. ```javascript // Initial UI setup updateCopyButtons(); renderZonesList(); ``` -------------------------------- ### Toggle Zone Editing Function Source: https://github.com/alti3/hexpicker/blob/master/index.html Enables or disables the editing mode for a specific zone. When editing starts, it disables certain UI controls (like creating new zones) and shows a 'Finish Editing' button. When editing finishes, it restores the UI controls. ```javascript function toggleEditZone(zoneId) { if (editingZoneId === zoneId) { finishEditing(); return; } if (editingZoneId !== null) return; editingZoneId = zoneId; drawButton.disabled = true; resolutionSelect.disabled = true; createZoneButton.disabled = true; importGeoButton.disabled = true; finishEditButton.style.display = "block"; renderZonesList(); } finishEditButton.addEventListener("click", finishEditing); function finishEditing() { editingZoneId = null; drawButton.disabled = false; resolutionSelect.disabled = false; createZoneButton.disabled = selectedCells.size === 0; importGeoButton.disabled = false; finishEditButton.style.display = "none"; renderZonesList(); } ``` -------------------------------- ### Importing Libraries and Initializing Map in HexPicker Source: https://github.com/alti3/hexpicker/blob/master/index.html This snippet demonstrates how to import necessary libraries (Leaflet, Leaflet-Draw, h3-js) using ESM CDN links and initializes the Leaflet map with specified center and tile layer. It also sets up global constants and initial layer groups. ```javascript // Use ESM versions from a CDN. We pin Leaflet for the Draw plugin with ?deps=. import * as L from "https://esm.sh/leaflet@1.9.4"; import "https://esm.sh/leaflet-draw@1.0.4?deps=leaflet@1.9.4"; import * as h3 from "https://esm.sh/h3-js@4.1.0"; const DEFAULT_RESOLUTION = 8; const MAP_CENTER = [32.85, 13.1913]; const HEX_COLOR_REGEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/; let h3Resolution = DEFAULT_RESOLUTION; const map = L.map("map").setView(MAP_CENTER, 13); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OpenStreetMap contributors", }).addTo(map); // Layers const cellsLayer = L.layerGroup().addTo(map); const zoneOutlinesLayer = L.layerGroup().addTo(map); // Data stores let drawnCells = new Set(); let cellPolygons = {}; let selectedCells = new Set(); let zones = []; let hexToZone = {}; let editingZoneId = null; let zoneCounter = 0; ``` -------------------------------- ### Create Zone Button Event Listener Source: https://github.com/alti3/hexpicker/blob/master/index.html Handles the click event for creating a new zone. It prompts the user for a unique zone name and color, validates the inputs, assigns selected hexagons to the new zone, applies styling, and updates the UI. It prevents overlapping zones and ensures unique names and colors. ```javascript createZoneButton.addEventListener("click", () => { if (selectedCells.size === 0) return; let name; while (true) { name = prompt("Zone name:"); if (name === null) return; name = name.trim(); if (name === "") { alert("Zone name cannot be empty. Please enter a name."); continue; } if (zones.some((z) => z.name.toLowerCase() === name.toLowerCase())) { alert("Zone name must be unique. Please try a different name."); continue; } break; } let color; while (true) { color = prompt("Zone color (#RRGGBB or #RGB):", "#ff0000"); if (color === null) return; if (!isValidHexColor(color)) { alert("Color must be a valid hex code starting with # (e.g., #ff0000 or #f00)."); continue; } if (zones.some((z) => z.color.toLowerCase() === color.toLowerCase())) { alert("Zone color must be unique. Please choose a different color."); continue; } break; } const overlap = Array.from(selectedCells).filter((h) => hexToZone[h] !== undefined); if (overlap.length) { alert("Some selected hexagons are already assigned to zones."); return; } const area = computeArea(selectedCells); const zone = { id: zoneCounter++, name, color, hexagons: new Set(selectedCells), area, outlinePolys: [] }; zones.push(zone); zone.hexagons.forEach((hex) => { hexToZone[hex] = zone.id; const poly = cellPolygons[hex]; if (poly) { applyZoneStyle(poly, zone); poly.inZone = true; } }); drawZoneOutline(zone); selectedCells.clear(); updateHexagonCounter(); renderZonesList(); }); ``` -------------------------------- ### Create Zone from Selection (JavaScript) Source: https://context7.com/alti3/hexpicker/llms.txt Converts a set of selected hexagons into a named, color-coded zone. It calculates the total area, prompts the user for a unique name and color, checks for overlaps with existing zones, updates hexagon styles, and draws an outline for the new zone. Dependencies include the H3 library for geospatial indexing and Leaflet for map rendering. ```javascript let zones = []; let hexToZone = {}; let zoneCounter = 0; // Compute total area from hexagon set const computeArea = (hexSet) => { let total = 0; hexSet.forEach((h) => (total += h3.cellArea(h, "m2"))); return total; }; // Create zone button handler document.getElementById("createZoneButton").addEventListener("click", () => { if (selectedCells.size === 0) { alert("No hexagons selected"); return; } // Prompt for unique zone name let name; while (true) { name = prompt("Zone name:"); if (name === null) return; name = name.trim(); if (name === "") { alert("Zone name cannot be empty"); continue; } if (zones.some((z) => z.name.toLowerCase() === name.toLowerCase())) { alert("Zone name must be unique"); continue; } break; } // Prompt for unique color let color; while (true) { color = prompt("Zone color (#RRGGBB or #RGB):", "#ff0000"); if (color === null) return; const HEX_COLOR_REGEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/; if (!HEX_COLOR_REGEX.test(color)) { alert("Invalid hex color format"); continue; } if (zones.some((z) => z.color.toLowerCase() === color.toLowerCase())) { alert("Zone color must be unique"); continue; } break; } // Check for overlaps const overlap = Array.from(selectedCells).filter( (h) => hexToZone[h] !== undefined ); if (overlap.length) { alert("Some hexagons are already assigned to zones"); return; } // Create zone const area = computeArea(selectedCells); const zone = { id: zoneCounter++, name, color, hexagons: new Set(selectedCells), area, outlinePolys: [] }; zones.push(zone); // Update mappings and styles zone.hexagons.forEach((hex) => { hexToZone[hex] = zone.id; const poly = cellPolygons[hex]; if (poly) { poly.setStyle({ fillOpacity: 0.5, fillColor: zone.color, weight: 0 }); poly.inZone = true; } }); // Draw zone outline const multiLngLat = h3.cellsToMultiPolygon(Array.from(zone.hexagons), true); multiLngLat.forEach((polyLngLat) => { const latlngs = polyLngLat.map((ringLngLat) => ringLngLat.map(([lng, lat]) => [lat, lng]) ); const outline = L.polygon(latlngs, { color: zone.color, weight: 3, fillOpacity: 0, interactive: false }); zoneOutlinesLayer.addLayer(outline); zone.outlinePolys.push(outline); }); selectedCells.clear(); console.log(`Zone "${name}" created with ${zone.hexagons.size} hexagons`); }); ``` -------------------------------- ### Import GeoJSON and Process Zones (JavaScript) Source: https://context7.com/alti3/hexpicker/llms.txt Handles the import of GeoJSON files, converts polygons to H3 hexagonal cells, organizes cells into zones, and updates the global state. It includes error handling for file reading and processing, and provides user feedback via alerts. ```javascript const reader = new FileReader(); reader.onload = (event) => { try { const geojsonData = JSON.parse(event.target.result); const features = geojsonData.features; let importedCellCount = 0; const importedZonesBatch = []; let zoneCounter = zones.length; features.forEach(feature => { const { properties, geometry } = feature; const zoneName = properties.name || `Zone ${zoneCounter}`; const zoneColor = properties.color || '#ffffff'; const zoneH3Cells = new Set(); const hexToZone = {}; // Assumed to be defined globally or passed in const zones = []; // Assumed to be defined globally or passed in const redrawGrid = () => {}; // Placeholder for function const computeArea = (cells) => 0; // Placeholder for function const h3 = { cellToBoundary: (cell) => [[0,0]] }; // Mock H3 object // Simplified processing for demonstration // In a real scenario, this would involve H3 conversion from geometry if (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') { // Assume cells are derived from geometry here const cell = 'some_h3_cell'; // Placeholder if (hexToZone[cell] === undefined) { let cellInBatch = false; for (const bz of importedZonesBatch) { if (bz.hexagons.has(cell)) { cellInBatch = true; break; } } if (!cellInBatch) { zoneH3Cells.add(cell); } } } }); if (zoneH3Cells.size > 0) { importedZonesBatch.push({ id: -1, name: zoneName, color: zoneColor, hexagons: zoneH3Cells, area: computeArea(zoneH3Cells), outlinePolys: [] }); importedCellCount += zoneH3Cells.size; } } catch (h3Error) { console.error("Error converting polygon to H3 cells:", h3Error); } }); if (zoneH3Cells.size > 0) { importedZonesBatch.push({ id: -1, name: zoneName, color: zoneColor, hexagons: zoneH3Cells, area: computeArea(zoneH3Cells), outlinePolys: [] }); importedCellCount += zoneH3Cells.size; } // Add imported zones to global state importedZonesBatch.forEach(newZoneData => { newZoneData.id = zoneCounter++; zones.push(newZoneData); newZoneData.hexagons.forEach(hex => { hexToZone[hex] = newZoneData.id; }); }); if (importedZonesBatch.length > 0) { redrawGrid(); alert( `Imported ${importedZonesBatch.length} zones with ${importedCellCount} cells` ); } else { alert("No zones imported. Check console for details."); } } catch (error) { console.error("Error importing GeoJSON:", error); alert(`Error importing GeoJSON: ${error.message}`); } finally { event.target.value = null; } }; reader.onerror = () => { alert("Error reading file"); event.target.value = null; }; reader.readAsText(file); }; ``` -------------------------------- ### Import GeoJSON Zones and Handle H3 Resolution Source: https://github.com/alti3/hexpicker/blob/master/index.html This JavaScript code handles the import of GeoJSON files, parses the data, validates its structure (must be a FeatureCollection with features), and checks for H3 resolution mismatches. It prompts the user to confirm resolution changes and existing zone overwrites. It then iterates through features, extracts zone names and colors, generates H3 cells using `h3.polygonToCells`, handles potential overlaps with existing or newly imported zones, and adds the new zones to the application's state. Includes error handling for file reading and GeoJSON parsing. ```javascript const reader = new FileReader(); reader.onload = (e) => { try { const geojsonData = JSON.parse(e.target.result); if (geojsonData.type !== "FeatureCollection" || !Array.isArray(geojsonData.features)) throw new Error("Invalid GeoJSON: Must be a FeatureCollection with a 'features' array."); if (geojsonData.features.length === 0) { alert("No features found in the GeoJSON file."); event.target.value = null; return; } const importResolution = geojsonData.features[0]?.properties?.h3_resolution; if (importResolution !== undefined && importResolution !== h3Resolution) { if (!confirm(`The imported GeoJSON was created with H3 resolution ${importResolution}, but your current resolution is ${h3Resolution}. Cells will be generated using resolution ${h3Resolution}. This may lead to different results. Continue?`)) { event.target.value = null; return; } } if (zones.length > 0) { if (!confirm("Importing new GeoJSON will clear all existing zones. Continue?")) { event.target.value = null; return; } clearAllZones(); if (selectedCells.size) clearButton.click(); } else { genericZoneNameCounter = 1; defaultColorIndex = 0; } const importedZonesBatch = []; let importedCellCount = 0; for (const feature of geojsonData.features) { if (feature.type !== "Feature" || !feature.geometry || feature.geometry.type !== "MultiPolygon") { console.warn("Skipping invalid feature:", feature); continue; } let zoneName = feature.properties?.zone_name; let zoneColor = feature.properties?.zone_color; if (!zoneName || String(zoneName).trim() === "") { zoneName = generateUniqueZoneName(zones, importedZonesBatch); } else { zoneName = String(zoneName).trim(); if (zones.some(z => z.name.toLowerCase() === zoneName.toLowerCase()) || importedZonesBatch.some(nz => nz.name.toLowerCase() === zoneName.toLowerCase())) { console.warn(`Zone name "${zoneName}" from file already exists or is duplicated. Assigning a generic name.`); zoneName = generateUniqueZoneName(zones, importedZonesBatch); } } if (!zoneColor || !isValidHexColor(zoneColor)) { if (zoneColor) console.warn(`Invalid color "${zoneColor}" for zone "${zoneName}". Assigning default.`); zoneColor = generateUniqueZoneColor(zones, importedZonesBatch); } else { if (zones.some(z => z.color.toLowerCase() === zoneColor.toLowerCase()) || importedZonesBatch.some(nz => nz.color.toLowerCase() === zoneColor.toLowerCase())) { console.warn(`Zone color "${zoneColor}" for zone "${zoneName}" already exists or is duplicated. Assigning new default.`); zoneColor = generateUniqueZoneColor(zones, importedZonesBatch); } } const zoneH3Cells = new Set(); for (const polygonGeoJsonCoords of feature.geometry.coordinates) { if (polygonGeoJsonCoords && polygonGeoJsonCoords.length > 0 && polygonGeoJsonCoords[0].length >= 3) { try { const cellsFromPolygon = h3.polygonToCells(polygonGeoJsonCoords, h3Resolution, true); cellsFromPolygon.forEach(cell => { if (hexToZone[cell] !== undefined) { const existingGlobalZone = zones.find(z => z.id === hexToZone[cell]); console.warn(`Hex ${cell} for "${zoneName}" already in existing zone "${existingGlobalZone?.name}". Skipping cell.`); } else { let cellInBatch = false; for (const bz of importedZonesBatch) { if (bz.hexagons.has(cell)) { console.warn(`Hex ${cell} for "${zoneName}" already in another new zone "${bz.name}" from this import. Skipping cell.`); cellInBatch = true; break; } } if (!cellInBatch) zoneH3Cells.add(cell); } }); } catch (h3Error) { console.error(`Error converting polygon to H3 cells for zone "${zoneName}":`, h3Error, "Polygon data:", polygonGeoJsonCoords); } } else { console.warn(`Skipping malformed polygon within MultiPolygon for zone "${zoneName}".`); } } if (zoneH3Cells.size > 0) { importedZonesBatch.push({ id: -1, name: zoneName, color: zoneColor, hexagons: zoneH3Cells, area: computeArea(zoneH3Cells), outlinePolys: [] }); importedCellCount += zoneH3Cells.size; } else { console.warn(`No valid, non-overlapping H3 cells generated for zone "${zoneName}". It might be too small, outside map, or entirely overlapping.`); } } importedZonesBatch.forEach(newZoneData => { newZoneData.id = zoneCounter++; zones.push(newZoneData); newZoneData.hexagons.forEach(hex => { hexToZone[hex] = newZoneData.id; }); }); if (importedZonesBatch.length > 0) { redrawGrid(); renderZonesList(); alert(`Successfully imported ${importedZonesBatch.length} zone(s) with a total of ${importedCellCount} unique cells.`); } else { alert("Import complete, but no new zones were added. Check console for details if features were expected."); } } catch (error) { console.error("Error importing GeoJSON:", error); alert(`Error importing GeoJSON: ${error.message}`); } finally { event.target.value = null; } }; reader.onerror = () => { alert("Error reading file."); event.target.value = null; }; reader.readAsText(file); ``` -------------------------------- ### Generate Unique Zone Names and Colors Source: https://github.com/alti3/hexpicker/blob/master/index.html Provides utility functions for creating unique zone names and colors when importing data. It checks against existing zones to avoid duplicates and cycles through a predefined list of default colors, falling back to random colors if necessary. ```javascript let genericZoneNameCounter = 1; const defaultColors = ["#E6194B", "#3CB44B", "#FFE119", "#4363D8", "#F58231", "#911EB4", "#46F0F0", "#F032E6", "#BCF60C", "#FABEBE", "#008080", "#E6BEFF", "#9A6324", "#FFFAC8", "#800000", "#AAFFC3", "#808000", "#FFD8B1", "#000075", "#808000", "#808080"] let defaultColorIndex = 0; function generateUniqueZoneName(currentGlobalZones, tempNewZonesList) { let name; do { name = `Zone ${genericZoneNameCounter++}`; } while (currentGlobalZones.some(z => z.name.toLowerCase() === name.toLowerCase()) || tempNewZonesList.some(nz => nz.name.toLowerCase() === name.toLowerCase())); return name; } ``` ```javascript function generateUniqueZoneColor(currentGlobalZones, tempNewZonesList) { let color; let attempts = 0; const initialIndex = defaultColorIndex; do { color = defaultColors[defaultColorIndex % defaultColors.length]; defaultColorIndex++; attempts++; if (attempts > defaultColors.length && defaultColorIndex % defaultColors.length === initialIndex % defaultColors.length) break; } while ((currentGlobalZones.some(z => z.color.toLowerCase() === color.toLowerCase()) || tempNewZonesList.some(nz => nz.color.toLowerCase() === color.toLowerCase())) && attempts < defaultColors.length * 2); if (currentGlobalZones.some(z => z.color.toLowerCase() === color.toLowerCase()) || tempNewZonesList.some(nz => nz.color.toLowerCase() === color.toLowerCase())) { do { color = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'); } while (currentGlobalZones.some(z => z.color.toLowerCase() === color.toLowerCase()) || tempNewZonesList.some(nz => nz.color.toLowerCase() === color.toLowerCase())); } return color; } ``` -------------------------------- ### DOM Element References for HexPicker UI Controls Source: https://github.com/alti3/hexpicker/blob/master/index.html This snippet lists all the references to HTML elements that serve as UI controls within the HexPicker application. These include buttons for drawing, clearing, creating zones, editing, copying data, and input elements for resolution and file uploads. ```javascript const drawButton = document.getElementById("drawButton"); const clearButton = document.getElementById("clearButton"); const createZoneButton = document.getElementById("createZoneButton"); const finishEditButton = document.getElementById("finishEditButton"); const copyIdsButton = document.getElementById("copyIdsButton"); const copyGeoButton = document.getElementById("copyGeoButton"); const resolutionSelect = document.getElementById("resolutionSelect"); const hexagonCounter = document.getElementById("hexagonCounter"); const zonesContainer = document.getElementById("zonesContainer"); const exportGeoButton = document.getElementById("exportGeoButton"); const importGeoButton = document.getElementById("importGeoButton"); const geojsonFileInput = document.getElementById("geojsonFile"); const zonesSummaryEl = document.getElementById("zonesSummary"); ``` -------------------------------- ### Initialize Leaflet Map with H3 Grid in JavaScript Source: https://context7.com/alti3/hexpicker/llms.txt Initializes a Leaflet map with H3 hexagonal tessellation for interactive zone management. It uses vanilla JavaScript and the h3-js library to render cells based on the current viewport and allows for interactive selection of hexagons. ```javascript import * as L from "https://esm.sh/leaflet@1.9.4"; import * as h3 from "https://esm.sh/h3-js@4.1.0"; // Configuration const DEFAULT_RESOLUTION = 8; const MAP_CENTER = [32.85, 13.1913]; // Initialize map const map = L.map("map").setView(MAP_CENTER, 13); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OpenStreetMap contributors", }).addTo(map); // Create layer groups const cellsLayer = L.layerGroup().addTo(map); const zoneOutlinesLayer = L.layerGroup().addTo(map); // Data structures let h3Resolution = DEFAULT_RESOLUTION; let drawnCells = new Set(); let cellPolygons = {}; let selectedCells = new Set(); // Draw H3 cells for current viewport function drawH3Cells() { const bounds = map.getBounds(); const nw = bounds.getNorthWest(); const se = bounds.getSouthEast(); const hexagonsToDraw = h3.polygonToCells( [ [nw.lat, nw.lng], [se.lat, nw.lng], [se.lat, se.lng], [nw.lat, se.lng], ], h3Resolution, false ); hexagonsToDraw.forEach((hex) => { if (drawnCells.has(hex)) return; const coords = h3.cellToBoundary(hex).map(([lat, lng]) => [lat, lng]); const polygon = L.polygon(coords, { color: "blue", weight: 1, fillOpacity: 0 }); polygon.h3Index = hex; // Interactive handlers polygon.on("click", function () { toggleSelection(this.h3Index, this); }); cellPolygons[hex] = polygon; cellsLayer.addLayer(polygon); drawnCells.add(hex); }); } // Redraw on map movement map.on("moveend", drawH3Cells); drawH3Cells(); ``` -------------------------------- ### Rendering Zone List in HexPicker UI Source: https://github.com/alti3/hexpicker/blob/master/index.html This JavaScript function, `renderZonesList`, is responsible for dynamically populating the `zonesContainer` element with visual representations of created zones. Each zone is displayed with a color swatch, name, hexagon count, and area, and includes controls for editing and deleting. ```javascript function renderZonesList() { zonesContainer.innerHTML = ""; zones.forEach((z) => { const item = document.createElement("div"); item.className = "zone-item"; const left = document.createElement("div"); left.className = "zone-left"; const swatch = document.createElement("span"); swatch.className = "color-swatch"; swatch.style.background = z.color; swatch.title = "Click to change color"; swatch.addEventListener("click", () => changeZoneColor(z.id)); const nameWrap = document.createElement("div"); const nameSpan = document.createElement("div"); nameSpan.textContent = z.name; nameSpan.className = "zone-name"; const metaSpan = document.createElement("div"); metaSpan.textContent = `${z.hexagons.size} hex • ${fmtInt(z.area)} m²`; metaSpan.className = "zone-meta"; nameWrap.appendChild(nameSpan); nameWrap.appendChild(metaSpan); left.appendChild(swatch); left.appendChild(nameWrap); const actions = document.createElement("div"); actions.className = "action-btns"; // Edit button const editBtn = document.createElement("button"); editBtn.className = "edit-btn"; if (editingZoneId === z.id) { editBtn.innerHTML = ' { const multiLngLat = h3.cellsToMultiPolygon(Array.from(z.hexagons), true); return { type: "Feature", geometry: { type: "MultiPolygon", coordinates: multiLngLat }, properties: { zone_name: z.name, zone_color: z.color, hex_count: z.hexagons.size, area_m2: Math.round(z.area), h3_resolution: h3Resolution, }, }; }); return { type: "FeatureCollection", features }; } ``` ```javascript copyIdsButton.addEventListener("click", () => { const payload = zones.map((z) => ({ zone_name: z.name, zone_color: z.color, hex_count: z.hexagons.size, area_m2: Math.round(z.area), zone_cells: Array.from(z.hexagons), })); navigator.clipboard.writeText(JSON.stringify(payload, null, 2)) .then(() => alert("Zone cell IDs copied!")); }); ``` ```javascript copyGeoButton.addEventListener("click", () => { const geojson = getGeoJSONForAllZones(); navigator.clipboard.writeText(JSON.stringify(geojson, null, 2)) .then(() => alert("GeoJSON copied!")); }); ``` ```javascript exportGeoButton.addEventListener("click", () => { if (zones.length === 0) return; const geojson = getGeoJSONForAllZones(); const blob = new Blob([JSON.stringify(geojson, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "zones.geojson"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); ``` -------------------------------- ### Import GeoJSON Zones with Conflict Resolution (JavaScript) Source: https://context7.com/alti3/hexpicker/llms.txt Handles the import of GeoJSON files, including validation, conflict resolution for zone names and colors, and conversion of geometries to H3 cells. It requires the 'h3-js' library for H3 operations and assumes the existence of global variables like 'zones', 'h3Resolution', 'clearAllZones', and DOM elements for file input and import buttons. ```javascript const defaultColors = [ "#E6194B", "#3CB44B", "#FFE119", "#4363D8", "#F58231", "#911EB4", "#46F0F0", "#F032E6", "#BCF60C", "#FABEBE" ]; let defaultColorIndex = 0; let genericZoneNameCounter = 1; // Generate unique zone name function generateUniqueZoneName(currentGlobalZones, tempNewZonesList) { let name; do { name = `Zone ${genericZoneNameCounter++}`; } while ( currentGlobalZones.some(z => z.name.toLowerCase() === name.toLowerCase()) || tempNewZonesList.some(nz => nz.name.toLowerCase() === name.toLowerCase()) ); return name; } // Generate unique zone color function generateUniqueZoneColor(currentGlobalZones, tempNewZonesList) { let color; let attempts = 0; do { color = defaultColors[defaultColorIndex % defaultColors.length]; defaultColorIndex++; attempts++; } while ( (currentGlobalZones.some(z => z.color.toLowerCase() === color.toLowerCase()) || tempNewZonesList.some(nz => nz.color.toLowerCase() === color.toLowerCase())) && attempts < defaultColors.length * 2 ); // Fallback to random color if all defaults are taken if (currentGlobalZones.some(z => z.color.toLowerCase() === color.toLowerCase()) || tempNewZonesList.some(nz => nz.color.toLowerCase() === color.toLowerCase())) { do { color = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'); } while ( currentGlobalZones.some(z => z.color.toLowerCase() === color.toLowerCase()) || tempNewZonesList.some(nz => nz.color.toLowerCase() === color.toLowerCase()) ); } return color; } // Import handler document.getElementById("importGeoButton").addEventListener("click", () => { document.getElementById("geojsonFile").click(); }); document.getElementById("geojsonFile").addEventListener("change", (event) => { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const geojsonData = JSON.parse(e.target.result); // Validate structure if (geojsonData.type !== "FeatureCollection" || !Array.isArray(geojsonData.features)) { throw new Error("Invalid GeoJSON: Must be a FeatureCollection"); } if (geojsonData.features.length === 0) { alert("No features found in GeoJSON"); return; } // Check resolution mismatch const importResolution = geojsonData.features[0]?.properties?.h3_resolution; if (importResolution !== undefined && importResolution !== h3Resolution) { if (!confirm( `GeoJSON has resolution ${importResolution}, current is ${h3Resolution}. Continue?` )) { return; } } // Clear existing zones if present if (zones.length > 0) { if (!confirm("Importing will clear all existing zones. Continue?")) { return; } clearAllZones(); } const importedZonesBatch = []; let importedCellCount = 0; // Process each feature for (const feature of geojsonData.features) { if (feature.type !== "Feature" || feature.geometry?.type !== "MultiPolygon") { console.warn("Skipping invalid feature:", feature); continue; } // Get or generate zone name let zoneName = feature.properties?.zone_name; if (!zoneName || String(zoneName).trim() === "") { zoneName = generateUniqueZoneName(zones, importedZonesBatch); } else { zoneName = String(zoneName).trim(); if (zones.some(z => z.name.toLowerCase() === zoneName.toLowerCase()) || importedZonesBatch.some(nz => nz.name.toLowerCase() === zoneName.toLowerCase())) { zoneName = generateUniqueZoneName(zones, importedZonesBatch); } } // Get or generate zone color let zoneColor = feature.properties?.zone_color; const HEX_COLOR_REGEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/; if (!zoneColor || !HEX_COLOR_REGEX.test(zoneColor)) { zoneColor = generateUniqueZoneColor(zones, importedZonesBatch); } else if ( zones.some(z => z.color.toLowerCase() === zoneColor.toLowerCase()) || importedZonesBatch.some(nz => nz.color.toLowerCase() === zoneColor.toLowerCase()) ) { zoneColor = generateUniqueZoneColor(zones, importedZonesBatch); } // Convert geometry to H3 cells const zoneH3Cells = new Set(); for (const polygonGeoJsonCoords of feature.geometry.coordinates) { if (polygonGeoJsonCoords?.[0]?.length >= 3) { try { const cellsFromPolygon = h3.polygonToCells( polygonGeoJsonCoords, h3Resolution, true ); cellsFromPolygon.forEach(cell => { ``` -------------------------------- ### Utility Functions for HexPicker: Formatting and Validation Source: https://github.com/alti3/hexpicker/blob/master/index.html This code block defines utility functions for the HexPicker project, including number formatting, area computation using h3-js, and hexadecimal color validation. These functions are essential for data processing and UI feedback. ```javascript // -------- Utility -------- const fmtInt = (v) => v.toLocaleString(undefined, { maximumFractionDigits: 0 }); const computeArea = (hexSet) => { let total = 0; hexSet.forEach((h) => (total += h3.cellArea(h, "m2"))); return total; }; const isValidHexColor = (c) => HEX_COLOR_REGEX.test(c); ``` -------------------------------- ### Create Zone Action Buttons - JavaScript Source: https://github.com/alti3/hexpicker/blob/master/index.html This code creates action buttons (Edit, Rename, Locate, Delete) for each zone. Each button is associated with a specific zone ID and triggers a corresponding action when clicked. The buttons' disabled state is managed based on whether a zone is currently being edited. ```javascript // Edit button const editBtn = document.createElement("button"); editBtn.className = "edit-btn"; editBtn.title = editingZoneId === z.id ? 'Editing…' : "Edit Zone"; editBtn.innerHTML = editingZoneId === z.id ? ' Editing…' : ''; editBtn.classList.add("edit-active"); if (editingZoneId !== null && editingZoneId !== z.id) { editBtn.disabled = true; } else { editBtn.disabled = false; editBtn.title = "Edit Zone"; } editBtn.addEventListener("click", () => toggleEditZone(z.id)); // Rename button const renameBtn = document.createElement("button"); renameBtn.className = "rename-btn"; renameBtn.innerHTML = ''; renameBtn.title = "Rename Zone"; renameBtn.disabled = editingZoneId !== null && editingZoneId !== z.id; renameBtn.addEventListener("click", () => renameZone(z.id)); // Center/locate button const locateBtn = document.createElement("button"); locateBtn.className = "locate-btn"; locateBtn.innerHTML = ''; locateBtn.title = "Center map on zone"; locateBtn.disabled = editingZoneId !== null && editingZoneId !== z.id; locateBtn.addEventListener("click", () => zoomToZone(z.id)); // Delete button const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = ''; deleteBtn.title = "Delete Zone"; deleteBtn.className = "delete-btn"; deleteBtn.disabled = editingZoneId !== null; deleteBtn.addEventListener("click", () => deleteZone(z.id)); actions.appendChild(editBtn); actions.appendChild(renameBtn); actions.appendChild(locateBtn); actions.appendChild(deleteBtn); ``` -------------------------------- ### Export Zones to GeoJSON in JavaScript Source: https://context7.com/alti3/hexpicker/llms.txt Generates a GeoJSON FeatureCollection from defined zones, including geometry and properties. Provides functionality to copy the GeoJSON to the clipboard or download it as a file. Depends on the H3 library and browser Clipboard API. ```javascript // Generate GeoJSON for all zones function getGeoJSONForAllZones() { const features = zones.map((zone) => { const multiLngLat = h3.cellsToMultiPolygon( Array.from(zone.hexagons), true ); return { type: "Feature", geometry: { type: "MultiPolygon", coordinates: multiLngLat }, properties: { zone_name: zone.name, zone_color: zone.color, hex_count: zone.hexagons.size, area_m2: Math.round(zone.area), h3_resolution: h3Resolution, }, }; }); return { type: "FeatureCollection", features }; } // Copy to clipboard document.getElementById("copyGeoButton").addEventListener("click", () => { const geojson = getGeoJSONForAllZones(); navigator.clipboard.writeText(JSON.stringify(geojson, null, 2)) .then(() => alert("GeoJSON copied to clipboard")); }); // Download as file document.getElementById("exportGeoButton").addEventListener("click", () => { if (zones.length === 0) return; const geojson = getGeoJSONForAllZones(); const blob = new Blob( [JSON.stringify(geojson, null, 2)], { type: "application/json" } ); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "zones.geojson"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); // Example output structure: /* { "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "MultiPolygon", "coordinates": [[[ [13.1234, 32.8567], [13.1245, 32.8578], [13.1256, 32.8589] ]]] }, "properties": { "zone_name": "Downtown", "zone_color": "#ff0000", "hex_count": 45, "area_m2": 125678, "h3_resolution": 8 } } ] } */ ``` -------------------------------- ### Populating Resolution Dropdown in HexPicker Source: https://github.com/alti3/hexpicker/blob/master/index.html This JavaScript code iterates from 0 to 15 to create and append 'option' elements to a 'resolutionSelect' dropdown. It sets the default selected option to the 'DEFAULT_RESOLUTION'. ```javascript // Populate resolution dropdown for (let r = 0; r <= 15; r++) { const opt = document.createElement("option"); opt.value = r.toString(); opt.textContent = `Resolution ${r}`; if (r === DEFAULT_RESOLUTION) opt.selected = true; resolutionSelect.appendChild(opt); } ``` -------------------------------- ### Import GeoJSON Zones Source: https://github.com/alti3/hexpicker/blob/master/index.html Handles the import of GeoJSON files to create new zones. It triggers a file input dialog and processes the selected file. This functionality is disabled if a zone is currently being edited. ```javascript importGeoButton.addEventListener("click", () => { if (editingZoneId !== null) { alert("Please finish editing the current zone before importing."); return; } geojsonFileInput.click(); }); ``` ```javascript geojsonFileInput.addEventListener("change", (event) => { const file = event.target.files[0]; if (!file) return; c ``` -------------------------------- ### HexPicker UI Update Functions: Counters and Buttons Source: https://github.com/alti3/hexpicker/blob/master/index.html This section contains JavaScript functions designed to update the HexPicker user interface. It includes functions to update the hexagon counter, enable/disable control buttons based on selection or zone data, and render a summary of created zones. ```javascript function updateHexagonCounter() { hexagonCounter.textContent = `Selected Hexagons: ${selectedCells.size}`; clearButton.disabled = selectedCells.size === 0; createZoneButton.disabled = selectedCells.size === 0; } function updateCopyButtons() { const disabled = zones.length === 0; copyIdsButton.disabled = disabled; copyGeoButton.disabled = disabled; exportGeoButton.disabled = disabled; } function updateZonesSummary() { if (zones.length === 0) { zonesSummaryEl.innerHTML = ""; zonesSummaryEl.style.display = "none"; return; } let totalHexagons = 0; let totalArea = 0; zones.forEach(zone => { totalHexagons += zone.hexagons.size; totalArea += zone.area; }); zonesSummaryEl.innerHTML = ` Total Zones: ${zones.length}
Total Hexagons: ${fmtInt(totalHexagons)}
Total Area: ${fmtInt(totalArea)} m² `; zonesSummaryEl.style.display = "block"; } ``` -------------------------------- ### Center Map on Zone (JavaScript) Source: https://context7.com/alti3/hexpicker/llms.txt Provides a function to zoom the map view to fit a specific zone's geographic bounds. It prioritizes using pre-defined outline polygons and falls back to calculating bounds from H3 cell boundaries if outlines are not available. Requires Leaflet and H3 libraries. ```javascript // Zoom to zone's bounding box function zoomToZone(zoneId) { const zone = zones.find((z) => z.id === zoneId); if (!zone) return; // Ensure outlines exist for bounds calculation if (!zone.outlinePolys || zone.outlinePolys.length === 0) { const multiLngLat = h3.cellsToMultiPolygon( Array.from(zone.hexagons), true ); zone.outlinePolys = []; multiLngLat.forEach((polyLngLat) => { const latlngs = polyLngLat.map((ringLngLat) => ringLngLat.map(([lng, lat]) => [lat, lng]) ); const outline = L.polygon(latlngs, { color: zone.color, weight: 3, fillOpacity: 0, interactive: false }); zoneOutlinesLayer.addLayer(outline); zone.outlinePolys.push(outline); }); } // Calculate combined bounds from all outline polygons let combinedBounds = null; if (zone.outlinePolys && zone.outlinePolys.length) { zone.outlinePolys.forEach((poly) => { const b = poly.getBounds(); if (!combinedBounds) { combinedBounds = L.latLngBounds( b.getSouthWest(), b.getNorthEast() ); } else { combinedBounds.extend(b); } }); } else { // Fallback: compute bounds from cell boundaries const latlngs = []; zone.hexagons.forEach((hex) => { const boundary = h3.cellToBoundary(hex); boundary.forEach(([lat, lng]) => latlngs.push([lat, lng])); }); if (latlngs.length) { combinedBounds = L.latLngBounds( latlngs.map(([lat, lng]) => L.latLng(lat, lng)) ); } } if (combinedBounds) { map.fitBounds(combinedBounds, { padding: [40, 40] }); } } // Usage with button click document.querySelector(".locate-btn").addEventListener("click", () => { zoomToZone(targetZoneId); }); ``` -------------------------------- ### Apply Zone Styling Function Source: https://github.com/alti3/hexpicker/blob/master/index.html Applies visual styling to a hexagon polygon based on its assigned zone. Sets the fill opacity and color for the polygon, and resets the border weight to zero to integrate it visually into the zone. ```javascript function applyZoneStyle(polygon, zone) { polygon.setStyle({ fillOpacity: 0.5, fillColor: zone.color, weight: 0 }); } ```