tu-econmap / index.html
thanthamky's picture
Upload index.html
f324446 verified
<!DOCTYPE html>
<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: '&copy; <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 &copy; 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>