Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Leaflet GeoJSON Styler</title> | |
| <!-- 1. Leaflet CSS from CDN --> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" | |
| integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" | |
| crossorigin="" /> | |
| <!-- 2. Google Font (Inter) --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet"> | |
| <!-- 3. Embedded CSS --> | |
| <style> | |
| /* Basic page reset and font application */ | |
| html, body { | |
| height: 100%; | |
| width: 100%; | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Inter', sans-serif; | |
| background-color: #f0f0f0; | |
| } | |
| /* Full-page map container */ | |
| #map { | |
| height: 100%; | |
| width: 100%; | |
| /* Fixes potential z-index issue with Leaflet controls */ | |
| z-index: 1; | |
| } | |
| /* Styling for the top-left controls widget */ | |
| #controls { | |
| position: absolute; | |
| top: 10px; | |
| left: 50px; /* Aligned with Leaflet's zoom control */ | |
| z-index: 1000; /* Ensures it's above the map */ | |
| background: white; | |
| padding: 12px 15px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.2); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| max-width: 200px; | |
| } | |
| #controls h4 { | |
| margin: 0 0 5px 0; | |
| font-size: 14px; | |
| font-weight: 700; | |
| color: #333; | |
| } | |
| #controls label { | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: #555; | |
| display: block; | |
| margin-bottom: 3px; | |
| } | |
| /* Styling for inputs and select */ | |
| #controls input[type="text"], | |
| #controls input[type="color"], | |
| #controls select { | |
| width: 100%; | |
| box-sizing: border-box; /* Ensures padding doesn't affect width */ | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| padding: 6px; | |
| font-size: 12px; | |
| font-family: 'Inter', sans-serif; | |
| } | |
| #controls input[type="color"] { | |
| padding: 2px; /* Color inputs are weird */ | |
| height: 30px; | |
| } | |
| /* Styling for the bottom-right legend */ | |
| .legend { | |
| position: absolute; | |
| bottom: 30px; /* Aligned with Leaflet's attribution */ | |
| right: 10px; | |
| z-index: 1000; | |
| background: white; | |
| padding: 10px 12px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.2); | |
| line-height: 1.5; | |
| font-size: 13px; | |
| color: #333; | |
| } | |
| .legend h4 { | |
| margin-top: 0; | |
| margin-bottom: 5px; | |
| font-size: 14px; | |
| font-weight: 700; | |
| } | |
| /* Style for the color box in the legend */ | |
| .legend i { | |
| width: 18px; | |
| height: 18px; | |
| float: left; | |
| margin-right: 8px; | |
| opacity: 0.9; | |
| border: 1px solid #777; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- 4. HTML Map Container --> | |
| <div id="map"></div> | |
| <!-- 5. HTML Controls Widget --> | |
| <div id="controls"> | |
| <h4>GeoJSON Style</h4> | |
| <div> | |
| <label for="colorizeProperty">Colorize Property</label> | |
| <input type="text" id="colorizeProperty" value="pop_max"> | |
| </div> | |
| <div> | |
| <label for="colormap">Colormap</label> | |
| <select id="colormap"> | |
| <option value="Reds">Reds</option> | |
| <option value="Greens">Greens</option> | |
| <option value="Blues">Blues</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="strokeColor">Stroke Color</label> | |
| <input type="color" id="strokeColor" value="#000000"> | |
| </div> | |
| <div> | |
| <label for="tooltipProperty">Tooltip Property</label> | |
| <input type="text" id="tooltipProperty" value="name"> | |
| </div> | |
| </div> | |
| <!-- 6. Leaflet JS from CDN --> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" | |
| integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" | |
| crossorigin=""></script> | |
| <!-- 7. Embedded Application JavaScript --> | |
| <script> | |
| // Wait for the DOM to be fully loaded before running script | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // --- 1. GLOBAL VARIABLES --- | |
| let map; | |
| let geoJsonLayer; | |
| let legend; | |
| const geoJsonUrl = 'https://huggingface.co/spaces/thanthamky/tu-econmap/resolve/main/hhincome.geojson'; | |
| // DOM elements for controls | |
| const controls = { | |
| colorizeProperty: document.getElementById('colorizeProperty'), | |
| colormap: document.getElementById('colormap'), | |
| strokeColor: document.getElementById('strokeColor'), | |
| tooltipProperty: document.getElementById('tooltipProperty') | |
| }; | |
| // --- 2. MAP INITIALIZATION --- | |
| // Define base map layers | |
| const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' | |
| }); | |
| const googleSat = L.tileLayer('http://www.google.cn/maps/vt?lyrs=s&x={x}&y={y}&z={z}', { | |
| attribution: 'Map data © Google' | |
| }); | |
| // Initialize the map | |
| map = L.map('map', { | |
| center: [13.7563, 100.5018], // Centered on Bangkok | |
| zoom: 6, | |
| layers: [osm] // Default layer | |
| }); | |
| // Layer control objects | |
| const baseMaps = { | |
| "OpenStreetMap": osm, | |
| "Google Satellite": googleSat | |
| }; | |
| const overlayMaps = {}; // Will be populated by fetch | |
| // Add layer control to map | |
| const layerControl = L.control.layers(baseMaps, overlayMaps, { | |
| position: 'topright', | |
| collapsed: true | |
| }).addTo(map); | |
| // --- 3. LEGEND CONTROL --- | |
| // Create a custom control for the legend | |
| const LegendControl = L.Control.extend({ | |
| options: { | |
| position: 'bottomright' | |
| }, | |
| onAdd: function (map) { | |
| const container = L.DomUtil.create('div', 'legend'); | |
| container.innerHTML = ` | |
| <h4>Legend</h4> | |
| <div><i style="background: #a50f15"></i> Legends</div> | |
| <!-- This is a static placeholder as requested --> | |
| `; | |
| return container; | |
| } | |
| }); | |
| // Add the legend to the map | |
| legend = new LegendControl().addTo(map); | |
| // --- 4. HELPER FUNCTIONS --- | |
| /** | |
| * Gets the min and max values of a property from GeoJSON features. | |
| * @param {string} property - The name of the property to analyze. | |
| * @param {Array} features - An array of GeoJSON features. | |
| * @returns {object} - An object { min, max }. | |
| */ | |
| function getMinMax(property, features) { | |
| const values = features | |
| .map(f => f.properties[property]) | |
| .filter(v => typeof v === 'number' && isFinite(v)); | |
| if (values.length === 0) { | |
| return { min: 0, max: 1 }; // Default if no valid data | |
| } | |
| return { | |
| min: Math.min(...values), | |
| max: Math.max(...values) | |
| }; | |
| } | |
| /** | |
| * Simple linear interpolation for a single color channel. | |
| * @param {number} val - Normalized value (0 to 1). | |
| * @param {number} start - Start channel value (0-255). | |
| * @param {number} end - End channel value (0-255). | |
| * @returns {number} - Interpolated channel value. | |
| */ | |
| function lerp(val, start, end) { | |
| return Math.round(start + (end - start) * val); | |
| } | |
| /** | |
| * Converts RGB values to a hex string. | |
| * @param {number} r - Red (0-255). | |
| * @param {number} g - Green (0-255). | |
| * @param {number} b - Blue (0-255). | |
| * @returns {string} - Hex color code (e.g., "#ff0000"). | |
| */ | |
| function toHex(r, g, b) { | |
| return "#" + [r, g, b] | |
| .map(x => { | |
| const hex = x.toString(16); | |
| return hex.length === 1 ? '0' + hex : hex; | |
| }) | |
| .join(''); | |
| } | |
| /** | |
| * Generates a color from a colormap based on a value. | |
| * @param {number} value - The feature's value. | |
| * @param {number} min - The minimum value in the dataset. | |
| * @param {number} max - The maximum value in the dataset. | |
| * @param {string} colormapName - "Reds", "Greens", or "Blues". | |
| * @returns {string} - Hex color code. | |
| */ | |
| function getColor(value, min, max, colormapName) { | |
| if (typeof value !== 'number' || !isFinite(value)) { | |
| return '#808080'; // Default gray for non-numeric data | |
| } | |
| // Normalize the value | |
| const range = max - min; | |
| let normValue = (range === 0) ? 0.5 : (value - min) / range; | |
| normValue = Math.max(0, Math.min(1, normValue)); // Clamp 0-1 | |
| let r, g, b; | |
| const start = 255; // White | |
| switch (colormapName) { | |
| case 'Reds': | |
| r = start; | |
| g = lerp(normValue, start, 0); // White to Red | |
| b = lerp(normValue, start, 0); | |
| break; | |
| case 'Greens': | |
| r = lerp(normValue, start, 0); | |
| g = start; | |
| b = lerp(normValue, start, 0); // White to Green | |
| break; | |
| case 'Blues': | |
| default: | |
| r = lerp(normValue, start, 0); | |
| g = lerp(normValue, start, 0); | |
| b = start; // White to Blue | |
| break; | |
| } | |
| return toHex(r, g, b); | |
| } | |
| /** | |
| * Dynamically styles a single GeoJSON feature based on current controls. | |
| * @param {object} feature - The GeoJSON feature. | |
| * @returns {object} - A Leaflet path style object. | |
| */ | |
| function styleFeature(feature) { | |
| const colorizeProp = controls.colorizeProperty.value; | |
| const colormap = controls.colormap.value; | |
| const stroke = controls.strokeColor.value; | |
| // Get min/max from the full dataset (stored on the layer) | |
| const { min, max } = geoJsonLayer.minMax[colorizeProp]; | |
| const value = feature.properties[colorizeProp]; | |
| const fillColor = getColor(value, min, max, colormap); | |
| return { | |
| fillColor: fillColor, | |
| color: stroke, | |
| weight: 1, | |
| opacity: 1, | |
| fillOpacity: 0.7 | |
| }; | |
| } | |
| // --- 5. STYLING AND TOOLTIP UPDATE FUNCTIONS --- | |
| /** | |
| * Re-applies the style to the entire GeoJSON layer. | |
| */ | |
| function updateStyle() { | |
| if (!geoJsonLayer) return; // Guard clause | |
| // Re-calculate min/max for the new property if needed | |
| const prop = controls.colorizeProperty.value; | |
| if (!geoJsonLayer.minMax[prop]) { | |
| geoJsonLayer.minMax[prop] = getMinMax(prop, geoJsonLayer.toGeoJSON().features); | |
| } | |
| // Re-style the layer | |
| geoJsonLayer.setStyle(styleFeature); | |
| } | |
| /** | |
| * Updates the tooltips for all features on the layer. | |
| */ | |
| function updateTooltips() { | |
| if (!geoJsonLayer) return; // Guard clause | |
| const prop = controls.tooltipProperty.value; | |
| geoJsonLayer.eachLayer((layer) => { | |
| const content = layer.feature.properties[prop]; | |
| const tooltipContent = (content !== null && content !== undefined) ? String(content) : 'N/A'; | |
| layer.unbindTooltip(); | |
| layer.bindTooltip(tooltipContent); | |
| }); | |
| } | |
| // --- 6. DATA FETCHING AND LAYER CREATION --- | |
| fetch(geoJsonUrl) | |
| .then(response => { | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| return response.json(); | |
| }) | |
| .then(data => { | |
| // Create the GeoJSON layer | |
| geoJsonLayer = L.geoJSON(data, { | |
| // style: styleFeature, // <-- REMOVED: Don't style until minMax is calculated | |
| onEachFeature: (feature, layer) => { | |
| // Bind initial tooltip | |
| const prop = controls.tooltipProperty.value; | |
| const content = feature.properties[prop]; | |
| const tooltipContent = (content !== null && content !== undefined) ? String(content) : 'N/A'; | |
| layer.bindTooltip(tooltipContent); | |
| } | |
| }); | |
| // Pre-calculate min/max for the default property | |
| // Store minMax on the layer object for access in styleFeature | |
| geoJsonLayer.minMax = {}; | |
| const defaultProp = controls.colorizeProperty.value; | |
| geoJsonLayer.minMax[defaultProp] = getMinMax(defaultProp, data.features); | |
| // NOW that the layer and minMax exist, apply the style | |
| geoJsonLayer.setStyle(styleFeature); | |
| // Add layer to the map and layer control | |
| geoJsonLayer.addTo(map); | |
| layerControl.addOverlay(geoJsonLayer, "Household Income layer"); | |
| // Now that the layer exists, apply the initial style | |
| // updateStyle(); // <-- REMOVED: This is now redundant | |
| }) | |
| .catch(error => { | |
| console.error("Error fetching or processing GeoJSON:", error); | |
| alert("Could not load GeoJSON data. See console for details."); | |
| }); | |
| // --- 7. EVENT LISTENERS --- | |
| // Add listeners to update style | |
| controls.colorizeProperty.addEventListener('change', updateStyle); | |
| controls.colormap.addEventListener('change', updateStyle); | |
| controls.strokeColor.addEventListener('change', updateStyle); | |
| // Add listener to update tooltips | |
| controls.tooltipProperty.addEventListener('change', updateTooltips); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |