CoolFace
Apppublic

zakhtar/ACLED_MLL_Application

sourceHugging Faceupdated 1y agoView on Hugging Face
1likes
app.py1027 linesDownload Raw Back to root
1import os2from flask import Flask, render_template_string3 4app = Flask(__name__)5 6# Load from environment7mapbox_token = os.environ.get("mapbox_access_token", "")8print("Mapbox token", mapbox_token)9mapbox_styleid = os.environ.get("mapbox_styleid", "")10 11@app.route("/")12def index():13    html = """14    <!DOCTYPE html>15    <html lang="en">16    <head>17        <meta charset="UTF-8">18        <meta name="viewport" content="width=device-width, initial-scale=1.0">19        <title>MLL and ACLED Map</title>20        <script src='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js'></script>21        <link href='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css' rel='stylesheet' />22        <script src='https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js'></script>23        <script src='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js'></script>24        <link rel='stylesheet' href='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css' type='text/css' />25        <style>26            27            body { 28                margin: 0; 29                padding: 0; 30                font-family: Arial, sans-serif;31            }32            33            #map { 34                position: absolute; 35                top: 0; 36                bottom: 0; 37                width: 100%; 38            }39            40            .map-overlay {41                position: absolute;42                top: 10px;43                left: 10px;44                background: rgba(255, 255, 255, 0.95);45                padding: 15px;46                border-radius: 8px;47                box-shadow: 0 2px 10px rgba(0,0,0,0.2);48                z-index: 1000;49                min-width: 280px;50                max-height: 80vh;51                overflow-y: auto;52            }53    54            .location-filter-container {55                margin-bottom: 15px;56                border: 1px solid #ddd;57                border-radius: 4px;58                padding: 10px;59                background: #fafafa;60            }61    62            .location-search {63                width: 100%;64                padding: 6px;65                margin-bottom: 8px;66                border: 1px solid #ddd;67                border-radius: 4px;68                font-size: 13px;69                box-sizing: border-box;70            }71    72            .location-controls {73                display: flex;74                gap: 5px;75                margin-bottom: 8px;76            }77    78            .location-controls button {79                flex: 1;80                padding: 5px 8px;81                font-size: 11px;82                background: #e0e0e0;83                border: 1px solid #ccc;84                border-radius: 3px;85                cursor: pointer;86            }87    88            .location-controls button:hover {89                background: #d0d0d0;90            }91    92            .location-list {93                max-height: 200px; /* Crucial for virtualization */94                overflow-y: auto;95                border: 1px solid #ddd;96                border-radius: 4px;97                background: white;98                padding: 5px;99                margin-bottom: 8px; 100            }101    102            .location-item {103                display: flex;104                align-items: center;105                padding: 4px;106                font-size: 13px;107                cursor: pointer;108            }109    110            .location-item:hover {111                background: #f0f0f0;112            }113    114            .location-item input[type="checkbox"] {115                margin-right: 8px;116                cursor: pointer;117            }118    119            .location-item label {120                margin: 0;121                cursor: pointer;122                flex: 1;123                font-weight: normal;124            }125    126            .filter-container {127                margin-bottom: 15px;128            }129            130            label {131                display: block;132                margin-bottom: 5px;133                font-weight: bold;134                color: #333;135            }136            137            input[type="file"], select {138                width: 100%;139                padding: 8px;140                border: 1px solid #ddd;141                border-radius: 4px;142                font-size: 14px;143                box-sizing: border-box;144            }145            146            select:focus {147                outline: none;148                border-color: #4264fb;149            }150            151            .btn {152                background: #4264fb;153                color: white;154                border: none;155                padding: 8px 12px;156                border-radius: 4px;157                cursor: pointer;158                font-size: 12px;159                margin-top: 8px;160                width: 100%;161            }162            163            .btn:hover {164                background: #3451d6;165            }166            167            .clear-btn {168                background: #f44336;169            }170            171            .clear-btn:hover {172                background: #d32f2f;173            }174            175            .info {176                font-size: 12px;177                color: #666;178                margin-top: 10px;179                padding: 10px;180                background: #f5f5f5;181                border-radius: 4px;182            }183        </style>184    </head>185    <body>186        <div id='map'></div>187        188        <div class='map-overlay'>189            <h3 style="margin-top: 0;">MLL and ACLED Map</h3>190            191            <div class="filter-container">192                <label for="csv-file">Upload CSV File:</label>193                <input type="file" id="csv-file" accept=".csv">194                <button class="btn" onclick="loadCSVFile()">Load CSV</button>195            </div>196            197            <div class="location-filter-container" id="location-filter-container" style="display: none;">198                <label style="margin-bottom: 8px;">Filter by Location(s) Checkbox List:</label>199                <input type="text" class="location-search" id="location-search" placeholder="Search locations...">200                <div class="location-controls">201                    <button onclick="selectAllLocations(false)">Select All</button>202                    <button onclick="deselectAllLocations(false)">Deselect All</button>203                </div>204                <div class="location-list" id="location-list">205                    </div>206                 <button class="btn" onclick="zoomToSelectedLocationFilter()" style="margin-top: 0;">Apply Selection and Zoom</button>207                 <button class="btn" onclick="showAllPoints()" style="margin-top: 8px; background: #2196F3;">Show All Points</button>208            </div>209    210            <div class="filter-container">211                <label for="country-filter">Filter by Country:</label>212                <select id="country-filter" disabled>213                    <option value="">All Countries</option>214                </select>215                <button class="btn clear-btn" onclick="clearCountryFilter()">Clear Filter</button>216            </div>217    218            <div class="filter-container">219                <label for="admin1-filter">Filter by Admin1:</label>220                <select id="admin1-filter" disabled>221                    <option value="">All Admin1</option>222                </select>223            </div>224    225            <div class="filter-container">226                <label for="admin2-filter">Filter by Admin2:</label>227                <select id="admin2-filter" disabled>228                    <option value="">All Admin2</option>229                </select>230            </div>231    232            <div class="filter-container">233                <label for="admin3-filter">Filter by Admin3:</label>234                <select id="admin3-filter" disabled>235                    <option value="">All Admin3</option>236                </select>237            </div>238    239            <div class="filter-container">240                <label for="location-filter">Filter by Location:</label>241                <select id="location-filter" disabled>242                    <option value="">All Locations</option>243                </select>244            </div>245    246            <div class="info" id="feature-count">247                Please upload a CSV file to begin248            </div>249            250    251        </div>252    253        <script>254            mapboxgl.accessToken = "{{ mapbox_token }}";255            const map = new mapboxgl.Map({256                container: 'map',257                style: 'mapbox://styles/acled/{{mapbox_styleid}}',258                center: [0, 20],259                zoom: 2260            });261            262            let csvData = null;263            let countries = new Set();264            265            map.on('load', function() {266                map.addControl(new mapboxgl.NavigationControl(), 'top-right');267                map.addControl(new mapboxgl.FullscreenControl(), 'top-right');268                269                // Add geocoder with coordinate parsing270                const geocoder = new MapboxGeocoder({271                    accessToken: mapboxgl.accessToken,272                    mapboxgl: mapboxgl,273                    marker: {274                        color: 'red'275                    },276                    placeholder: 'Search or enter coordinates (lat, lon)',277                    proximity: {278                        longitude: 0,279                        latitude: 20280                    },281                    localGeocoder: coordinateFeature,282                    reverseGeocode: true283                });284                285                map.addControl(geocoder, 'top-right');286            });287            288            function loadCSVFile() {289                const fileInput = document.getElementById('csv-file');290                const file = fileInput.files[0];291                292                if (!file) {293                    alert('Please select a CSV file first');294                    return;295                }296                297                document.getElementById('feature-count').innerHTML = 'Loading and parsing CSV...';298    299                Papa.parse(file, {300                    header: true,301                    dynamicTyping: true,302                    skipEmptyLines: true,303                    complete: function(results) {304                        processCSVData(results.data);305                    },306                    error: function(error) {307                        alert('Error parsing CSV: ' + error.message);308                    }309                });310            }311            312            function processCSVData(data) {313                csvData = data;314                countries.clear();315                316                const features = [];317                318                data.forEach((row, index) => {319                    const lat = row.LATITUDE || row.latitude || row.Latitude || row.lat || row.LAT;320                    const lon = row.LONGITUDE || row.longitude || row.Longitude || row.lon || row.LON || row.lng || row.LNG;321                    322                    if (lat && lon && !isNaN(lat) && !isNaN(lon)) {323                        const country = row.COUNTRY || row.country || row.Country;324                        if (country) {325                            countries.add(country);326                        }327                        328                        features.push({329                            type: 'Feature',330                            geometry: {331                                type: 'Point',332                                coordinates: [parseFloat(lon), parseFloat(lat)]333                            },334                            properties: {335                                id: index,336                                COUNTRY: row.COUNTRY || row.country || row.Country || 'N/A',337                                ADMIN1: row.ADMIN1 || row.admin1 || row.Admin1 || 'N/A',338                                ADMIN2: row.ADMIN2 || row.admin2 || row.Admin2 || 'N/A',339                                ADMIN3: row.ADMIN3 || row.admin3 || row.Admin3 || 'N/A',340                                LOCATION: row.LOCATION || row.location || row.Location || 'N/A',341                                LOCATION_ID: row.LOCATION_ID || row.location_id || row.Location_Id || 'N/A'342                            }343                        });344                    }345                });346                347                if (features.length === 0) {348                    alert('No valid coordinates found in CSV. Please ensure your CSV has LATITUDE and LONGITUDE columns.');349                    return;350                }351                352                const geojson = {353                    type: 'FeatureCollection',354                    features: features355                };356                357                if (map.getSource('csv-points')) {358                    map.removeLayer('csv-points-layer');359                    map.removeSource('csv-points');360                }361                362                map.addSource('csv-points', {363                    type: 'geojson',364                    data: geojson365                });366                367                map.addLayer({368                    id: 'csv-points-layer',369                    type: 'circle',370                    source: 'csv-points',371                    paint: {372                        'circle-radius': 6,373                        'circle-color': '#4264fb',374                        'circle-stroke-width': 2,375                        'circle-stroke-color': '#ffffff'376                    }377                });378                379                // Add click popup and hover effects (omitted for brevity, assume they are correct)380    381                map.on('click', 'csv-points-layer', function(e) {382                    const props = e.features[0].properties;383                    384                    new mapboxgl.Popup()385                        .setLngLat(e.lngLat)386                        .setHTML(`387                            <div style="font-family: Arial, sans-serif;">388                                <h3 style="margin: 0 0 10px 0; color: #4264fb;">Location Details</h3>389                                <p style="margin: 0; font-size: 12px; color: #333;">390                                    <strong>COUNTRY:</strong> ${props.COUNTRY}<br>391                                    <strong>ADMIN1:</strong> ${props.ADMIN1}<br>392                                    <strong>ADMIN2:</strong> ${props.ADMIN2}<br>393                                    <strong>ADMIN3:</strong> ${props.ADMIN3}<br>394                                    <strong>LOCATION:</strong> ${props.LOCATION}<br>395                                    <strong>LOCATION_ID:</strong> ${props.LOCATION_ID}<br>396                                </p>397                            </div>398                        `)399                        .addTo(map);400                });401                402                map.on('mouseenter', 'csv-points-layer', function(e) {403                    map.getCanvas().style.cursor = 'pointer';404                    const props = e.features[0].properties;405                    const popup = new mapboxgl.Popup({406                        closeButton: false,407                        closeOnClick: false,408                        offset: [0, -10]409                    })410                    .setLngLat(e.lngLat)411                    .setHTML(`412                        <div style="font-family: Arial, sans-serif; font-size: 14px; padding: 5px; color: #666;">413                        <div style="margin-bottom: 3px;"><strong>Country:</strong> ${props.COUNTRY}</div>414                        <div style="margin-bottom: 3px;"><strong>Admin1:</strong> ${props.ADMIN1}</div>415                        <div style="margin-bottom: 3px;"><strong>Admin2:</strong> ${props.ADMIN2}</div>416                        <div style="margin-bottom: 3px;"><strong>Admin3:</strong> ${props.ADMIN3}</div>417                        <div style="margin-bottom: 3px;"><strong>Location:</strong> ${props.LOCATION}</div>418                        <div style="margin-bottom: 3px;"><strong>Location ID:</strong> ${props.LOCATION_ID}</div>419                        </div>420                    `)421                    .addTo(map);422                    map._hoverPopup = popup;423                });424    425                map.on('mouseleave', 'csv-points-layer', function() {426                    map.getCanvas().style.cursor = '';427                    if (map._hoverPopup) {428                        map._hoverPopup.remove();429                        map._hoverPopup = null;430                    }431                });432                433                populateCountryDropdown();434                435                const coordinates = features.map(f => f.geometry.coordinates);436                const bounds = new mapboxgl.LngLatBounds();437                coordinates.forEach(coord => bounds.extend(coord));438                439                map.fitBounds(bounds, {440                    padding: 50,441                    duration: 1000442                });443                444                populateLocationFilter();445                updateFeatureCount();446            }447    448            let allLocations = new Set();449            let selectedLocations = new Set();450            let currentFilteredLocations = []; // Locations matching the search term451            452            // --- CHUNKED RENDERING / VIRTUALIZATION VARIABLES ---453            const RENDER_CHUNK_SIZE = 500; // Load 500 locations at a time454            let locationsRendered = 0;455            let isRendering = false;456            let searchTimeout;457            // -----------------------------------------------------458    459 460            function populateLocationFilter() {461                allLocations.clear();462                463                csvData.forEach(row => {464                    const location = row.LOCATION || row.location || row.Location;465                    if (location && location !== 'N/A') {466                        allLocations.add(location);467                    }468                });469                470                document.getElementById('location-filter-container').style.display = 'block';471                472                // Reset search box473                document.getElementById('location-search').value = '';474                475                // Select all by default in the background476                selectAllLocations(false); 477                478                // Initial render479                renderLocationList();480            }481 482            // This is the function that resets and starts the rendering process483            function renderLocationList(searchTerm = '') {484                const locationList = document.getElementById('location-list');485    486                // 1. Filter the master list based on search term487                const sortedLocations = Array.from(allLocations).sort();488                currentFilteredLocations = sortedLocations.filter(loc => 489                    loc.toLowerCase().includes(searchTerm.toLowerCase())490                );491                492                // 2. Reset rendering state493                locationsRendered = 0;494                locationList.innerHTML = ''; // Clear the list495                496                // 3. Detach and re-attach scroll listener if needed497                locationList.removeEventListener('scroll', handleScrollLoading);498                if (currentFilteredLocations.length > RENDER_CHUNK_SIZE) {499                    locationList.addEventListener('scroll', handleScrollLoading);500                }501    502                // 4. Render the initial chunk503                appendLocationChunk(locationList, RENDER_CHUNK_SIZE);504    505                // 5. Attach single event listener using delegation (only necessary once)506                if (!locationList.dataset.listenersAttached) {507                    locationList.onclick = handleCheckboxClick;508                    locationList.dataset.listenersAttached = 'true';509                }510            }511    512            // This is the function that adds the next chunk of items to the DOM513            function appendLocationChunk(locationList, chunkSize) {514                if (locationsRendered >= currentFilteredLocations.length || isRendering) return;515                516                isRendering = true;517                const fragment = document.createDocumentFragment();518                const chunk = currentFilteredLocations.slice(locationsRendered, locationsRendered + chunkSize);519                520                chunk.forEach((location, index) => {521                    const div = document.createElement('div');522                    div.className = 'location-item';523                    524                    const checkbox = document.createElement('input');525                    checkbox.type = 'checkbox';526                    checkbox.id = `loc-${locationsRendered + index}`; 527                    checkbox.value = location;528                    // Check against the master selection Set529                    checkbox.checked = selectedLocations.has(location);530                    checkbox.dataset.location = location;531                    532                    const label = document.createElement('label');533                    label.htmlFor = `loc-${locationsRendered + index}`;534                    label.textContent = location;535                    536                    div.appendChild(checkbox);537                    div.appendChild(label);538                    fragment.appendChild(div);539                });540                541                locationList.appendChild(fragment);542                locationsRendered += chunk.length;543                isRendering = false;544            }545    546            // Event handler for scroll-loading (simplified virtualization)547            function handleScrollLoading(e) {548                const list = e.target;549                // Check if user has scrolled near the bottom (10px buffer) AND not already rendering550                if (list.scrollTop + list.clientHeight >= list.scrollHeight - 10 && !isRendering) {551                    appendLocationChunk(list, RENDER_CHUNK_SIZE);552                }553            }554    555            // Event handler for checkbox clicks (delegation)556            function handleCheckboxClick(e) {557                 let target = e.target;558    559                // Find the checkbox regardless of whether the checkbox or label was clicked560                if (target.tagName === 'LABEL') {561                    target = target.previousSibling;562                }563                564                if (target.type === 'checkbox') {565                    const location = target.dataset.location;566                    if (location) {567                        if (target.checked) {568                            selectedLocations.add(location);569                        } else {570                            selectedLocations.delete(location);571                        }572                    }573                }574            }575            576            function selectAllLocations(applyFilter) {577                selectedLocations.clear();578                allLocations.forEach(loc => selectedLocations.add(loc));579                580                // Only update checkboxes for VISIBLE items581                const checkboxes = document.querySelectorAll('.location-item input[type="checkbox"]');582                checkboxes.forEach(cb => cb.checked = true);583                584                if (applyFilter) zoomToSelectedLocationFilter();585            }586    587            function deselectAllLocations(applyFilter) {588                selectedLocations.clear();589                590                // Only update checkboxes for VISIBLE items591                const checkboxes = document.querySelectorAll('.location-item input[type="checkbox"]');592                checkboxes.forEach(cb => cb.checked = false);593                594                if (applyFilter) zoomToSelectedLocationFilter();595            }596    597            598            // NEW FUNCTION: Handles button click to apply filter and zoom from the checkbox list599            function zoomToSelectedLocationFilter() {600                 if (!csvData) {601                     alert('Please load a CSV file first.');602                     return;603                }604    605                if (selectedLocations.size === 0) {606                     alert("Please select at least one location from the list.");607                     return;608                }609                610                // 1. Reset all cascading dropdown filters611                clearCountryFilter(false); 612                613                // 2. Apply the location filter (updates map filter)614                applyLocationFilter();615                616                // 3. Zoom617                zoomToFilteredPoints(true); 618            }619 620            // NEW FUNCTION: Shows all points without changing zoom level621            function showAllPoints() {622                if (!csvData) {623                    alert('Please load a CSV file first.');624                    return;625                }626                627                // 1. Clear all dropdown filters628                clearCountryFilter(false);629                630                // 2. Select all locations in the checkbox list631                selectAllLocations(false);632                633                // 3. Apply the filter (this will show all points)634                applyLocationFilter();635                636                // Note: We do NOT call zoomToFilteredPoints() to maintain current zoom637            }638 639            // Helper function to get the current filter criteria state640            function getFilterCriteria() {641                return {642                    selectedCountry: countrySelect.value,643                    selectedAdmin1: admin1Select.value,644                    selectedAdmin2: admin2Select.value,645                    selectedAdmin3: admin3Select.value,646                    cascadeLocation: locationSelect.value,647                    selectedLocations: selectedLocations, 648                    allLocations: allLocations 649                };650            }651            652            function coordinateFeature(query) {653                // Match coordinates in various formats654                const matches = query.match(/^[ ]*(?:Lat: )?(-?\d+\.?\d*)[, ]+(?:Lng: )?(-?\d+\.?\d*)[ ]*$/i);655                656                if (!matches) {657                    return null;658                }659                660                const lat = Number(matches[1]);661                const lon = Number(matches[2]);662                663                // Validate coordinate ranges664                if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {665                    return null;666                }667                668                // Return both coordinate orders as results669                return [670                    {671                        center: [lon, lat],672                        geometry: {673                            type: 'Point',674                            coordinates: [lon, lat]675                        },676                        place_name: `Lat: ${lat} Lng: ${lon}`,677                        place_type: ['coordinate'],678                        properties: {},679                        type: 'Feature'680                    },681                    {682                        center: [lat, lon],683                        geometry: {684                            type: 'Point',685                            coordinates: [lat, lon]686                        },687                        place_name: `Lat: ${lon} Lng: ${lat}`,688                        place_type: ['coordinate'],689                        properties: {},690                        type: 'Feature'691                    }692                ];693            }694 695            function zoomToFilteredPoints(useCheckboxFilter = false) {696                if (!csvData || csvData.length === 0) return;697    698                const criteria = getFilterCriteria();699                const bounds = new mapboxgl.LngLatBounds();700                let matchedFeatures = 0;701                702                const isDropdownFilterActive = criteria.selectedCountry || criteria.selectedAdmin1 || criteria.selectedAdmin2 || criteria.selectedAdmin3 || criteria.cascadeLocation;703    704                csvData.forEach(row => {705                    const rowCountry = row.COUNTRY || row.country || row.Country;706                    const rowAdmin1 = row.ADMIN1 || row.admin1 || row.Admin1;707                    const rowAdmin2 = row.ADMIN2 || row.admin2 || row.Admin2;708                    const rowAdmin3 = row.ADMIN3 || row.admin3 || row.Admin3;709                    const rowLocation = row.LOCATION || row.location || row.Location;710                    const lat = row.LATITUDE || row.latitude || row.Latitude || row.lat || row.LAT;711                    const lon = row.LONGITUDE || row.longitude || row.Longitude || row.lon || row.LNG;712    713                    let isMatch = true;714    715                    if (isDropdownFilterActive) {716                        if (criteria.selectedCountry && rowCountry !== criteria.selectedCountry) isMatch = false;717                        if (criteria.selectedAdmin1 && rowAdmin1 !== criteria.selectedAdmin1) isMatch = false;718                        if (criteria.selectedAdmin2 && rowAdmin2 !== criteria.selectedAdmin2) isMatch = false;719                        if (criteria.selectedAdmin3 && rowAdmin3 !== criteria.selectedAdmin3) isMatch = false;720                        if (criteria.cascadeLocation && rowLocation !== criteria.cascadeLocation) isMatch = false;721                    } else if (useCheckboxFilter && criteria.selectedLocations.size > 0 && criteria.selectedLocations.size < criteria.allLocations.size) {722                        if (!criteria.selectedLocations.has(rowLocation)) isMatch = false;723                    }724                    725                    if (isMatch && lat && lon && !isNaN(lat) && !isNaN(lon)) {726                        bounds.extend([parseFloat(lon), parseFloat(lat)]);727                        matchedFeatures++;728                    }729                });730    731                if (matchedFeatures > 0) {732                    map.fitBounds(bounds, {733                        padding: 50, 734                        duration: 800,735                        maxZoom: 14736                    });737                } else {738                     map.fitBounds([-180, -90, 180, 90], { padding: 50, duration: 800 });739                }740            }741    742    743            function applyLocationFilter() {744                if (!map.getSource('csv-points')) return;745                746                const selectedCountry = countrySelect.value;747                const selectedAdmin1 = admin1Select.value;748                const selectedAdmin2 = admin2Select.value;749                const selectedAdmin3 = admin3Select.value;750                const cascadeLocation = locationSelect.value;751                752                let filterArray = ['all'];753                754                if (selectedCountry) filterArray.push(['==', ['get', 'COUNTRY'], selectedCountry]);755                if (selectedAdmin1) filterArray.push(['==', ['get', 'ADMIN1'], selectedAdmin1]);756                if (selectedAdmin2) filterArray.push(['==', ['get', 'ADMIN2'], selectedAdmin2]);757                if (selectedAdmin3) filterArray.push(['==', ['get', 'ADMIN3'], selectedAdmin3]);758                if (cascadeLocation) filterArray.push(['==', ['get', 'LOCATION'], cascadeLocation]);759                760                // Apply checkbox filter ONLY if no dropdown filters are active (to prevent conflicts in the Mapbox filter array)761                const isDropdownFilterActive = selectedCountry || selectedAdmin1 || selectedAdmin2 || selectedAdmin3 || cascadeLocation;762    763                if (!isDropdownFilterActive && selectedLocations.size > 0 && selectedLocations.size < allLocations.size) {764                    filterArray.push(['in', ['get', 'LOCATION'], ['literal', Array.from(selectedLocations)]]);765                }766                767                map.setFilter('csv-points-layer', filterArray.length > 1 ? filterArray : null);768                769                clearTimeout(window.updateCountTimeout);770                window.updateCountTimeout = setTimeout(updateFeatureCount, 100);771            }772    773            function populateCountryDropdown() {774                const countrySelect = document.getElementById('country-filter');775                countrySelect.disabled = false;776                777                while (countrySelect.children.length > 1) {778                    countrySelect.removeChild(countrySelect.lastChild);779                }780                781                const sortedCountries = Array.from(countries).sort();782                783                sortedCountries.forEach(country => {784                    const option = document.createElement('option');785                    option.value = country;786                    option.textContent = country;787                    countrySelect.appendChild(option);788                });789            }790            791            // --- CASCADING FILTER LOGIC ---792            const countrySelect = document.getElementById('country-filter');793            const admin1Select = document.getElementById('admin1-filter');794            const admin2Select = document.getElementById('admin2-filter');795            const admin3Select = document.getElementById('admin3-filter');796            const locationSelect = document.getElementById('location-filter');797    798            function clearDropdown(selectElement) {799                while (selectElement.children.length > 1) {800                    selectElement.removeChild(selectElement.lastChild);801                }802                selectElement.value = '';803                selectElement.disabled = true;804            }805    806            // (Admin population functions remain the same for brevity)807    808            countrySelect.addEventListener('change', function(e) {809                const selectedCountry = e.target.value;810                clearDropdown(admin1Select);811                clearDropdown(admin2Select);812                clearDropdown(admin3Select);813                clearDropdown(locationSelect);814                if (selectedCountry) populateAdmin1Dropdown(selectedCountry);815                816                applyLocationFilter();817                zoomToFilteredPoints(false);818            });819    820            admin1Select.addEventListener('change', function(e) {821                const selectedAdmin1 = e.target.value;822                const selectedCountry = countrySelect.value;823                clearDropdown(admin2Select);824                clearDropdown(admin3Select);825                clearDropdown(locationSelect);826                if (selectedAdmin1) populateAdmin2Dropdown(selectedCountry, selectedAdmin1);827                828                applyLocationFilter();829                zoomToFilteredPoints(false);830            });831    832            admin2Select.addEventListener('change', function(e) {833                const selectedAdmin2 = e.target.value;834                const selectedCountry = countrySelect.value;835                const selectedAdmin1 = admin1Select.value;836                clearDropdown(admin3Select);837                clearDropdown(locationSelect);838                if (selectedAdmin2) populateAdmin3Dropdown(selectedCountry, selectedAdmin1, selectedAdmin2);839                840                applyLocationFilter();841                zoomToFilteredPoints(false);842            });843    844            admin3Select.addEventListener('change', function(e) {845                const selectedAdmin3 = e.target.value;846                const selectedCountry = countrySelect.value;847                const selectedAdmin1 = admin1Select.value;848                const selectedAdmin2 = admin2Select.value;849                clearDropdown(locationSelect);850                if (selectedAdmin3) populateLocationDropdown(selectedCountry, selectedAdmin1, selectedAdmin2, selectedAdmin3);851                852                applyLocationFilter();853                zoomToFilteredPoints(false);854            });855    856            locationSelect.addEventListener('change', function() {857                applyLocationFilter();858                zoomToFilteredPoints(false);859            });860            861            function clearCountryFilter(applyFilter = true) {862                 if (countrySelect) countrySelect.value = '';863                 clearDropdown(admin1Select);864                 clearDropdown(admin2Select);865                 clearDropdown(admin3Select);866                 clearDropdown(locationSelect); 867                 868                 if (applyFilter) {869                     applyLocationFilter();870                     zoomToFilteredPoints(false);871                 }872            }873    874            // (Admin population functions: included for completeness/context)875            function populateAdmin1Dropdown(selectedCountry) {876                const admin1s = new Set();877                csvData.forEach(row => {878                    const rowCountry = row.COUNTRY || row.country || row.Country;879                    const admin1 = row.ADMIN1 || row.admin1 || row.Admin1;880                    if (rowCountry === selectedCountry && admin1 && admin1 !== 'N/A') {881                        admin1s.add(admin1);882                    }883                });884                clearDropdown(admin1Select);885                if (admin1s.size > 0) {886                    Array.from(admin1s).sort().forEach(admin1 => {887                        const option = document.createElement('option');888                        option.value = admin1;889                        option.textContent = admin1;890                        admin1Select.appendChild(option);891                    });892                    admin1Select.disabled = false;893                }894            }895            function populateAdmin2Dropdown(selectedCountry, selectedAdmin1) {896                const admin2s = new Set();897                csvData.forEach(row => {898                    const rowCountry = row.COUNTRY || row.country || row.Country;899                    const rowAdmin1 = row.ADMIN1 || row.admin1 || row.Admin1;900                    const admin2 = row.ADMIN2 || row.admin2 || row.Admin2;901                    if (rowCountry === selectedCountry && rowAdmin1 === selectedAdmin1 && admin2 && admin2 !== 'N/A') {902                        admin2s.add(admin2);903                    }904                });905                clearDropdown(admin2Select);906                if (admin2s.size > 0) {907                    Array.from(admin2s).sort().forEach(admin2 => {908                        const option = document.createElement('option');909                        option.value = admin2;910                        option.textContent = admin2;911                        admin2Select.appendChild(option);912                    });913                    admin2Select.disabled = false;914                }915            }916            function populateAdmin3Dropdown(selectedCountry, selectedAdmin1, selectedAdmin2) {917                const admin3s = new Set();918                csvData.forEach(row => {919                    const rowCountry = row.COUNTRY || row.country || row.Country;920                    const rowAdmin1 = row.ADMIN1 || row.admin1 || row.Admin1;921                    const rowAdmin2 = row.ADMIN2 || row.admin2 || row.Admin2;922                    const admin3 = row.ADMIN3 || row.admin3 || row.Admin3;923                    if (rowCountry === selectedCountry && rowAdmin1 === selectedAdmin1 && rowAdmin2 === selectedAdmin2 && admin3 && admin3 !== 'N/A') {924                        admin3s.add(admin3);925                    }926                });927                clearDropdown(admin3Select);928                if (admin3s.size > 0) {929                    Array.from(admin3s).sort().forEach(admin3 => {930                        const option = document.createElement('option');931                        option.value = admin3;932                        option.textContent = admin3;933                        admin3Select.appendChild(option);934                    });935                    admin3Select.disabled = false;936                }937            }938            function populateLocationDropdown(selectedCountry, selectedAdmin1, selectedAdmin2, selectedAdmin3) {939                const locations = new Set();940                csvData.forEach(row => {941                    const rowCountry = row.COUNTRY || row.country || row.Country;942                    const rowAdmin1 = row.ADMIN1 || row.admin1 || row.Admin1;943                    const rowAdmin2 = row.ADMIN2 || row.admin2 || row.Admin2;944                    const rowAdmin3 = row.ADMIN3 || row.admin3 || row.Admin3;945                    const location = row.LOCATION || row.location || row.Location;946                    if (rowCountry === selectedCountry && rowAdmin1 === selectedAdmin1 && rowAdmin2 === selectedAdmin2 && rowAdmin3 === selectedAdmin3 && location && location !== 'N/A') {947                        locations.add(location);948                    }949                });950                clearDropdown(locationSelect);951                if (locations.size > 0) {952                    Array.from(locations).sort().forEach(location => {953                        const option = document.createElement('option');954                        option.value = location;955                        option.textContent = location;956                        locationSelect.appendChild(option);957                    });958                    locationSelect.disabled = false;959                }960            }961    962            function updateFeatureCount() {963                const countElement = document.getElementById('feature-count');964                965                if (!csvData) {966                    countElement.innerHTML = 'Please upload a CSV file to begin';967                    return;968                }969                970                const criteria = getFilterCriteria();971                972                const visibleCount = csvData.filter(row => {973                    const rowCountry = row.COUNTRY || row.country || row.Country;974                    const rowAdmin1 = row.ADMIN1 || row.admin1 || row.Admin1;975                    const rowAdmin2 = row.ADMIN2 || row.admin2 || row.Admin2;976                    const rowAdmin3 = row.ADMIN3 || row.admin3 || row.Admin3;977                    const rowLocation = row.LOCATION || row.location || row.Location;978    979                    const isDropdownFilterActive = criteria.selectedCountry || criteria.selectedAdmin1 || criteria.selectedAdmin2 || criteria.selectedAdmin3 || criteria.cascadeLocation;980    981                    if (criteria.selectedCountry && rowCountry !== criteria.selectedCountry) return false;982                    if (criteria.selectedAdmin1 && rowAdmin1 !== criteria.selectedAdmin1) return false;983                    if (criteria.selectedAdmin2 && rowAdmin2 !== criteria.selectedAdmin2) return false;984                    if (criteria.selectedAdmin3 && rowAdmin3 !== criteria.selectedAdmin3) return false;985                    if (criteria.cascadeLocation && rowLocation !== criteria.cascadeLocation) return false;986                    987                    // Only check location checkbox filter if no cascading dropdowns are active988                    if (!isDropdownFilterActive && criteria.selectedLocations.size > 0 && criteria.selectedLocations.size < criteria.allLocations.size) {989                        if (!criteria.selectedLocations.has(rowLocation)) return false;990                    }991                    992                    return true;993                }).length;994                995                const locationFilterText = criteria.selectedLocations.size < criteria.allLocations.size ? 996                    `<br><strong>Location Filter:</strong> ${criteria.selectedLocations.size} of ${criteria.allLocations.size} selected` : '';997                998                countElement.innerHTML = `999                    <strong>Total Points:</strong> ${csvData.length}<br>1000                    <strong>Countries:</strong> ${countries.size}<br>1001                    <strong>Visible Points:</strong> ${visibleCount}1002                    ${criteria.selectedCountry ? `<br><strong>Country:</strong> ${criteria.selectedCountry}` : ''}1003                    ${criteria.selectedAdmin1 ? `<br><strong>Admin1:</strong> ${criteria.selectedAdmin1}` : ''}1004                    ${criteria.selectedAdmin2 ? `<br><strong>Admin2:</strong> ${criteria.selectedAdmin2}` : ''}1005                    ${criteria.selectedAdmin3 ? `<br><strong>Admin3:</strong> ${criteria.selectedAdmin3}` : ''}1006                    ${criteria.cascadeLocation ? `<br><strong>Location:</strong> ${criteria.cascadeLocation}` : ''}1007                    ${locationFilterText}1008                `;1009            }1010            // Attach search listener after DOM is ready1011 1012            // Attach search listener1013            document.getElementById('location-search').addEventListener('input', function(e) {1014                clearTimeout(searchTimeout);1015                searchTimeout = setTimeout(() => {1016                    renderLocationList(e.target.value);1017                }, 300); 1018            });1019        </script>1020    </body>1021    </html>1022    """1023    return render_template_string(html, mapbox_token=mapbox_token,mapbox_styleid=mapbox_styleid)1024 1025if __name__ == "__main__":1026    app.run(host="0.0.0.0", port=7860)1027