zakhtar/ACLED_MLL_Application
1
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 