From fff5971ed04d524f661151bfcca8503b2d3ca963 Mon Sep 17 00:00:00 2001 From: escapedcat Date: Sat, 21 Feb 2026 12:24:22 +0100 Subject: [PATCH 1/5] feat: add Biome for frontend linting and formatting --- .github/workflows/lint.yml | 34 ++++ .gitignore | 8 + biome.json | 45 +++++ package.json | 14 ++ static/js/map-editor.js | 216 ++++++++++++++++-------- static/js/osm-geojson-generator.js | 254 ++++++++++++++++++----------- static/js/script.js | 135 +++++++++------ static/js/validation.js | 24 ++- 8 files changed, 503 insertions(+), 227 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 biome.json create mode 100644 package.json diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..2c639ad --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,34 @@ +name: Lint + +on: + pull_request: + paths: + - 'static/**' + - 'package.json' + - 'biome.json' + push: + branches: + - main + paths: + - 'static/**' + - 'package.json' + - 'biome.json' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run Biome + run: npm run lint diff --git a/.gitignore b/.gitignore index 6b5c196..d670b72 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,16 @@ instance/ # Virtual environment venv/ +.venv/ env/ +# Node.js +node_modules/ +package-lock.json + +# Biome +.biome/ + # IDEs and editors .vscode/ .idea/ diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..19a097d --- /dev/null +++ b/biome.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noForEach": "off", + "useOptionalChain": "off", + "useArrowFunction": "off" + }, + "style": { + "noParameterAssign": "off", + "noUselessElse": "off", + "useNumberNamespace": "off", + "useTemplate": "off", + "useExponentiationOperator": "off" + }, + "suspicious": { + "noAssignInExpressions": "off", + "noGlobalIsNan": "off" + } + }, + "ignore": ["**/.venv/**", "**/node_modules/**", "**/__pycache__/**"] + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 4, + "lineWidth": 80, + "ignore": ["**/.venv/**", "**/node_modules/**", "**/__pycache__/**"] + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingCommas": "es5", + "semicolons": "always", + "indentWidth": 4 + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b04d742 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "btcmap-admin", + "version": "0.1.0", + "description": "BTC Map Admin - Frontend dev tools", + "private": true, + "scripts": { + "lint": "biome check .", + "format": "biome format --write .", + "lint:fix": "biome check --write ." + }, + "devDependencies": { + "@biomejs/biome": "^1.9.0" + } +} diff --git a/static/js/map-editor.js b/static/js/map-editor.js index 690e83a..5f255fa 100644 --- a/static/js/map-editor.js +++ b/static/js/map-editor.js @@ -1,16 +1,16 @@ /** * Map Editor Component - * + * * Provides Leaflet map with draw controls for polygon editing, * shape editing tools (simplify/buffer/merge), and a raw GeoJSON text editor. - * + * * Usage: * const mapEditor = initMapEditor({ * containerId: 'map', * initialGeoJson: existingGeoJson || null, * onGeoJsonChange: (geometry) => { window.currentGeoJson = geometry; } * }); - * + * * Returns: * { * map: Leaflet map instance, @@ -18,7 +18,7 @@ * getCurrentGeoJson: () => geometry object or null, * updateGeoJson: (geojson) => void, * } - * + * * Requires: * - Leaflet and Leaflet.draw loaded * - Turf.js loaded (for shape editing) @@ -28,18 +28,18 @@ function initMapEditor(options = {}) { const { containerId = 'map', initialGeoJson = null, - onGeoJsonChange = null + onGeoJsonChange = null, } = options; // State let currentGeoJson = null; let geoJsonLayer = null; let previouslySavedGeoJson = null; - + // Shape editor state - let originalShapeGeoJson = null; // Original shape before editing - let shapePreviewLayer = null; // Preview layer for shape edits - let originalShapeLayer = null; // Reference layer showing original + let originalShapeGeoJson = null; // Original shape before editing + let shapePreviewLayer = null; // Preview layer for shape edits + let originalShapeLayer = null; // Reference layer showing original // DOM Elements const elements = { @@ -58,13 +58,15 @@ function initMapEditor(options = {}) { shapeSimplifyValue: document.getElementById('shape-simplify-value'), shapeBufferSlider: document.getElementById('shape-buffer-slider'), shapeBufferValue: document.getElementById('shape-buffer-value'), - shapeMegaSimplifyCheckbox: document.getElementById('shape-mega-simplify'), + shapeMegaSimplifyCheckbox: document.getElementById( + 'shape-mega-simplify' + ), shapeTightnessSlider: document.getElementById('shape-tightness-slider'), shapeTightnessValue: document.getElementById('shape-tightness-value'), shapePointsCount: document.getElementById('shape-points-count'), shapeShowOriginal: document.getElementById('shape-show-original'), applyShapeBtn: document.getElementById('apply-shape-btn'), - cancelShapeBtn: document.getElementById('cancel-shape-btn') + cancelShapeBtn: document.getElementById('cancel-shape-btn'), }; // Initialize map @@ -75,7 +77,7 @@ function initMapEditor(options = {}) { const map = L.map(containerId).setView([0, 0], 2); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' + attribution: '© OpenStreetMap contributors', }).addTo(map); // Initialize draw control @@ -89,25 +91,25 @@ function initMapEditor(options = {}) { showArea: true, shapeOptions: { color: '#3388ff', - weight: 3 - } + weight: 3, + }, }, rectangle: { shapeOptions: { color: '#3388ff', - weight: 3 - } + weight: 3, + }, }, circle: false, circlemarker: false, marker: false, - polyline: false + polyline: false, }, edit: { featureGroup: drawnItems, remove: true, - edit: true - } + edit: true, + }, }); map.addControl(drawControl); @@ -130,7 +132,11 @@ function initMapEditor(options = {}) { // Keep textarea in sync if (elements.geoJsonInput) { - elements.geoJsonInput.value = JSON.stringify(currentGeoJson, null, 2); + elements.geoJsonInput.value = JSON.stringify( + currentGeoJson, + null, + 2 + ); } map.fitBounds(drawnItems.getBounds()); @@ -171,7 +177,11 @@ function initMapEditor(options = {}) { let geometry = geoJson; if (geoJson.type === 'Feature') { geometry = geoJson.geometry; - } else if (geoJson.type === 'FeatureCollection' && geoJson.features && geoJson.features.length > 0) { + } else if ( + geoJson.type === 'FeatureCollection' && + geoJson.features && + geoJson.features.length > 0 + ) { geometry = geoJson.features[0].geometry; } @@ -180,8 +190,8 @@ function initMapEditor(options = {}) { // Wrap back as Feature for display in Leaflet const featureCollection = { - "type": "Feature", - "geometry": geometry + type: 'Feature', + geometry: geometry, }; // Create GeoJSON feature and add to map @@ -196,7 +206,11 @@ function initMapEditor(options = {}) { // Update textarea with current GeoJSON (keep them in sync) if (elements.geoJsonInput) { - elements.geoJsonInput.value = JSON.stringify(currentGeoJson, null, 2); + elements.geoJsonInput.value = JSON.stringify( + currentGeoJson, + null, + 2 + ); } if (onGeoJsonChange) { @@ -205,16 +219,20 @@ function initMapEditor(options = {}) { return true; } catch (error) { - console.error("[MapEditor] Error adding GeoJSON to map:", error); + console.error('[MapEditor] Error adding GeoJSON to map:', error); if (typeof showToast === 'function') { - showToast('Error', `Invalid GeoJSON: ${error.message}`, 'error'); + showToast( + 'Error', + `Invalid GeoJSON: ${error.message}`, + 'error' + ); } return false; } } // Handle draw created - map.on(L.Draw.Event.CREATED, function(e) { + map.on(L.Draw.Event.CREATED, function (e) { const layer = e.layer; // Clear existing and add new drawnItems.clearLayers(); @@ -223,12 +241,12 @@ function initMapEditor(options = {}) { }); // Handle draw edited - map.on(L.Draw.Event.EDITED, function(e) { + map.on(L.Draw.Event.EDITED, function (e) { updateGeoJsonFromDrawnItems(); }); // Handle draw deleted - map.on(L.Draw.Event.DELETED, function(e) { + map.on(L.Draw.Event.DELETED, function (e) { currentGeoJson = null; if (geoJsonLayer) { map.removeLayer(geoJsonLayer); @@ -248,7 +266,11 @@ function initMapEditor(options = {}) { elements.geoJsonEditor.style.display = 'block'; } if (currentGeoJson && elements.geoJsonInput) { - elements.geoJsonInput.value = JSON.stringify(currentGeoJson, null, 2); + elements.geoJsonInput.value = JSON.stringify( + currentGeoJson, + null, + 2 + ); previouslySavedGeoJson = JSON.parse(JSON.stringify(currentGeoJson)); } } @@ -268,9 +290,13 @@ function initMapEditor(options = {}) { } } } catch (error) { - console.error("[MapEditor] Error parsing GeoJSON:", error); + console.error('[MapEditor] Error parsing GeoJSON:', error); if (typeof showToast === 'function') { - showToast('Error', 'Invalid GeoJSON: ' + error.message, 'error'); + showToast( + 'Error', + 'Invalid GeoJSON: ' + error.message, + 'error' + ); } } } @@ -286,9 +312,13 @@ function initMapEditor(options = {}) { } } } catch (error) { - console.error("[MapEditor] Error saving GeoJSON:", error); + console.error('[MapEditor] Error saving GeoJSON:', error); if (typeof showToast === 'function') { - showToast('Error', 'Invalid GeoJSON: ' + error.message, 'error'); + showToast( + 'Error', + 'Invalid GeoJSON: ' + error.message, + 'error' + ); } } } @@ -327,7 +357,7 @@ function initMapEditor(options = {}) { // 10 = tighter fit (concavity = 1) function sliderToConcavity(sliderVal) { if (sliderVal === 0) return Infinity; - return Math.max(1, 21 - (sliderVal * 2)); + return Math.max(1, 21 - sliderVal * 2); } function formatTightness(val) { @@ -342,7 +372,7 @@ function initMapEditor(options = {}) { if (typeof coords[0] === 'number') { count++; } else { - coords.forEach(c => countCoords(c)); + coords.forEach((c) => countCoords(c)); } } const geometry = geojson.geometry || geojson; @@ -355,7 +385,11 @@ function initMapEditor(options = {}) { function showShapeEditor() { if (!currentGeoJson) { if (typeof showToast === 'function') { - showToast('Warning', 'No shape to edit. Draw or import a polygon first.', 'warning'); + showToast( + 'Warning', + 'No shape to edit. Draw or import a polygon first.', + 'warning' + ); } return; } @@ -416,14 +450,25 @@ function initMapEditor(options = {}) { originalShapeLayer = null; } - if (!originalShapeGeoJson || !elements.shapeShowOriginal || !elements.shapeShowOriginal.checked) { + if ( + !originalShapeGeoJson || + !elements.shapeShowOriginal || + !elements.shapeShowOriginal.checked + ) { return; } // Wrap as Feature if needed let feature = originalShapeGeoJson; - if (originalShapeGeoJson.type !== 'Feature' && originalShapeGeoJson.type !== 'FeatureCollection') { - feature = { type: 'Feature', geometry: originalShapeGeoJson, properties: {} }; + if ( + originalShapeGeoJson.type !== 'Feature' && + originalShapeGeoJson.type !== 'FeatureCollection' + ) { + feature = { + type: 'Feature', + geometry: originalShapeGeoJson, + properties: {}, + }; } // Create dashed line style for reference @@ -433,8 +478,8 @@ function initMapEditor(options = {}) { weight: 2, dashArray: '5, 5', fillOpacity: 0, - interactive: false - } + interactive: false, + }, }).addTo(map); } @@ -444,36 +489,54 @@ function initMapEditor(options = {}) { try { // Wrap as Feature if needed let feature = originalShapeGeoJson; - if (originalShapeGeoJson.type !== 'Feature' && originalShapeGeoJson.type !== 'FeatureCollection') { - feature = { type: 'Feature', geometry: originalShapeGeoJson, properties: {} }; + if ( + originalShapeGeoJson.type !== 'Feature' && + originalShapeGeoJson.type !== 'FeatureCollection' + ) { + feature = { + type: 'Feature', + geometry: originalShapeGeoJson, + properties: {}, + }; } - const tolerance = sliderToTolerance(parseFloat(elements.shapeSimplifySlider?.value || 0)); + const tolerance = sliderToTolerance( + parseFloat(elements.shapeSimplifySlider?.value || 0) + ); const buffer = parseFloat(elements.shapeBufferSlider?.value || 0); let processed = feature; // Apply buffer if > 0 if (buffer > 0 && typeof turf !== 'undefined') { - processed = turf.buffer(processed, buffer, { units: 'kilometers' }); + processed = turf.buffer(processed, buffer, { + units: 'kilometers', + }); } // Apply simplification if > 0 if (tolerance > 0 && typeof turf !== 'undefined') { processed = turf.simplify(processed, { tolerance: tolerance, - highQuality: true + highQuality: true, }); } // Apply mega simplify if checked - if (elements.shapeMegaSimplifyCheckbox?.checked && typeof turf !== 'undefined') { - const concavity = sliderToConcavity(parseFloat(elements.shapeTightnessSlider?.value || 0)); + if ( + elements.shapeMegaSimplifyCheckbox?.checked && + typeof turf !== 'undefined' + ) { + const concavity = sliderToConcavity( + parseFloat(elements.shapeTightnessSlider?.value || 0) + ); try { // Use turf.convex with concavity parameter - processed = turf.convex(processed, { concavity: concavity }); - + processed = turf.convex(processed, { + concavity: concavity, + }); + if (!processed) { console.warn('[MapEditor] Convex hull returned null'); processed = turf.convex(feature); @@ -490,7 +553,7 @@ function initMapEditor(options = {}) { } // Hide the main drawn items while previewing - drawnItems.eachLayer(layer => { + drawnItems.eachLayer((layer) => { layer.setStyle({ opacity: 0, fillOpacity: 0 }); }); @@ -500,8 +563,8 @@ function initMapEditor(options = {}) { color: '#3388ff', weight: 3, fillColor: '#3388ff', - fillOpacity: 0.2 - } + fillOpacity: 0.2, + }, }).addTo(map); // Fit bounds @@ -512,11 +575,14 @@ function initMapEditor(options = {}) { if (elements.shapePointsCount) { elements.shapePointsCount.textContent = `Points: ${count}`; } - } catch (error) { console.error('[MapEditor] Error processing shape:', error); if (typeof showToast === 'function') { - showToast('Error', `Error processing: ${error.message}`, 'error'); + showToast( + 'Error', + `Error processing: ${error.message}`, + 'error' + ); } } } @@ -532,7 +598,10 @@ function initMapEditor(options = {}) { // Get geometry from preview layer const previewGeoJson = shapePreviewLayer.toGeoJSON(); let geometry = previewGeoJson; - if (previewGeoJson.type === 'FeatureCollection' && previewGeoJson.features.length > 0) { + if ( + previewGeoJson.type === 'FeatureCollection' && + previewGeoJson.features.length > 0 + ) { geometry = previewGeoJson.features[0].geometry; } else if (previewGeoJson.type === 'Feature') { geometry = previewGeoJson.geometry; @@ -542,7 +611,7 @@ function initMapEditor(options = {}) { hideShapeEditor(); // Restore visibility of drawn items - drawnItems.eachLayer(layer => { + drawnItems.eachLayer((layer) => { layer.setStyle({ opacity: 1, fillOpacity: 0.2 }); }); @@ -558,7 +627,7 @@ function initMapEditor(options = {}) { hideShapeEditor(); // Restore visibility of drawn items - drawnItems.eachLayer(layer => { + drawnItems.eachLayer((layer) => { layer.setStyle({ opacity: 1, fillOpacity: 0.2 }); }); @@ -588,28 +657,32 @@ function initMapEditor(options = {}) { elements.editShapeBtn.addEventListener('click', showShapeEditor); } if (elements.shapeSimplifySlider) { - elements.shapeSimplifySlider.addEventListener('input', function() { + elements.shapeSimplifySlider.addEventListener('input', function () { const tolerance = sliderToTolerance(parseFloat(this.value)); - elements.shapeSimplifyValue.textContent = formatTolerance(tolerance); + elements.shapeSimplifyValue.textContent = + formatTolerance(tolerance); processAndPreviewShape(); }); } if (elements.shapeBufferSlider) { - elements.shapeBufferSlider.addEventListener('input', function() { + elements.shapeBufferSlider.addEventListener('input', function () { elements.shapeBufferValue.textContent = this.value; processAndPreviewShape(); }); } if (elements.shapeMegaSimplifyCheckbox) { - elements.shapeMegaSimplifyCheckbox.addEventListener('change', function() { - if (elements.shapeTightnessSlider) { - elements.shapeTightnessSlider.disabled = !this.checked; + elements.shapeMegaSimplifyCheckbox.addEventListener( + 'change', + function () { + if (elements.shapeTightnessSlider) { + elements.shapeTightnessSlider.disabled = !this.checked; + } + processAndPreviewShape(); } - processAndPreviewShape(); - }); + ); } if (elements.shapeTightnessSlider) { - elements.shapeTightnessSlider.addEventListener('input', function() { + elements.shapeTightnessSlider.addEventListener('input', function () { const val = parseInt(this.value); if (elements.shapeTightnessValue) { elements.shapeTightnessValue.textContent = formatTightness(val); @@ -618,7 +691,10 @@ function initMapEditor(options = {}) { }); } if (elements.shapeShowOriginal) { - elements.shapeShowOriginal.addEventListener('change', updateOriginalShapeLayer); + elements.shapeShowOriginal.addEventListener( + 'change', + updateOriginalShapeLayer + ); } if (elements.applyShapeBtn) { elements.applyShapeBtn.addEventListener('click', applyShapeChanges); @@ -644,7 +720,7 @@ function initMapEditor(options = {}) { showEditor, hideEditor, showShapeEditor, - hideShapeEditor + hideShapeEditor, }; } diff --git a/static/js/osm-geojson-generator.js b/static/js/osm-geojson-generator.js index 0c1a660..5b66481 100644 --- a/static/js/osm-geojson-generator.js +++ b/static/js/osm-geojson-generator.js @@ -1,16 +1,16 @@ /** * OSM GeoJSON Generator Component - * + * * Provides OSM search functionality with Turf.js processing * for simplification and buffering of boundaries. - * + * * Usage: * initOsmGeojsonGenerator({ * mapEditor: mapEditorInstance, * onApply: (geometry) => mapEditor.updateGeoJson(geometry), * onPopulationFound: (population, date) => { ... } * }); - * + * * Requires: * - Turf.js loaded * - mapEditor instance from map-editor.js @@ -22,7 +22,7 @@ function initOsmGeojsonGenerator(options = {}) { const { mapEditor = null, onApply = null, - onPopulationFound = null + onPopulationFound = null, } = options; if (!mapEditor) { @@ -48,15 +48,15 @@ function initOsmGeojsonGenerator(options = {}) { showOriginalBoundary: document.getElementById('show-original-boundary'), applyBtn: document.getElementById('apply-geojson-btn'), loading: document.getElementById('geojson-loading'), - error: document.getElementById('geojson-error') + error: document.getElementById('geojson-error'), }; // State - let originalOsmGeojson = null; // Full-detail from Nominatim - let processedGeojson = null; // After simplification/buffer - let originalBoundaryLayer = null; // Reference layer showing original - let previewLayer = null; // Preview layer for processed result - let osmSearchResults = []; // Store search results + let originalOsmGeojson = null; // Full-detail from Nominatim + let processedGeojson = null; // After simplification/buffer + let originalBoundaryLayer = null; // Reference layer showing original + let previewLayer = null; // Preview layer for processed result + let osmSearchResults = []; // Store search results // Helper functions function showError(message) { @@ -80,20 +80,20 @@ function initOsmGeojsonGenerator(options = {}) { function countGeojsonPoints(geojson) { let count = 0; - + function countCoords(coords) { if (typeof coords[0] === 'number') { count++; } else { - coords.forEach(c => countCoords(c)); + coords.forEach((c) => countCoords(c)); } } - + const geometry = geojson.geometry || geojson; if (geometry && geometry.coordinates) { countCoords(geometry.coordinates); } - + return count; } @@ -102,8 +102,8 @@ function initOsmGeojsonGenerator(options = {}) { function sliderToTolerance(sliderVal) { if (sliderVal === 0) return 0; // Map 1-10 to 0.000001-0.01 logarithmically (6 orders of magnitude) - const minLog = Math.log10(0.000001); // -6 - const maxLog = Math.log10(0.01); // -2 + const minLog = Math.log10(0.000001); // -6 + const maxLog = Math.log10(0.01); // -2 const logVal = minLog + (sliderVal / 10) * (maxLog - minLog); return Math.pow(10, logVal); } @@ -124,7 +124,7 @@ function initOsmGeojsonGenerator(options = {}) { if (sliderVal === 0) return Infinity; // Pure convex hull // Map 1-10 to concavity values (higher = looser, lower = tighter) // 1 = 20 (loose), 10 = 1 (tight) - return Math.max(1, 21 - (sliderVal * 2)); + return Math.max(1, 21 - sliderVal * 2); } function formatTightness(val) { @@ -142,37 +142,41 @@ function initOsmGeojsonGenerator(options = {}) { } return; } - + hideError(); elements.resultsContainer.style.display = 'none'; elements.controls.style.display = 'none'; showLoading(true); - + try { - const response = await apiFetch(`/api/search_osm?q=${encodeURIComponent(query)}`); + const response = await apiFetch( + `/api/search_osm?q=${encodeURIComponent(query)}` + ); const results = await response.json(); - + if (results.error) { throw new Error(results.error); } - + if (results.length === 0) { - showError('No administrative areas found. Try a different search term.'); + showError( + 'No administrative areas found. Try a different search term.' + ); return; } - + // Store results and populate dropdown osmSearchResults = results; - elements.resultsSelect.innerHTML = ''; + elements.resultsSelect.innerHTML = + ''; results.forEach((r, index) => { const option = document.createElement('option'); option.value = index; option.textContent = r.display_name; elements.resultsSelect.appendChild(option); }); - + elements.resultsContainer.style.display = 'block'; - } catch (error) { console.error('[OsmGeojsonGenerator] Search error:', error); showError(error.message || 'Search failed'); @@ -185,13 +189,13 @@ function initOsmGeojsonGenerator(options = {}) { async function onSearchResultSelect() { const index = elements.resultsSelect.value; if (index === '') return; - + const result = osmSearchResults[parseInt(index)]; if (!result || !result.geojson) { showError('Selected place has no boundary data'); return; } - + // Check if there's already a polygon, confirm before replacing const currentGeoJson = mapEditor.getCurrentGeoJson(); if (currentGeoJson && mapEditor.drawnItems.getLayers().length > 0) { @@ -201,25 +205,31 @@ function initOsmGeojsonGenerator(options = {}) { return; } } - + // Store the original GeoJSON originalOsmGeojson = result.geojson; - + // Handle population data from OSM extratags - if (result.extratags && result.extratags.population && onPopulationFound) { + if ( + result.extratags && + result.extratags.population && + onPopulationFound + ) { const populationValue = parseInt(result.extratags.population, 10); if (!isNaN(populationValue)) { const today = new Date().toISOString().split('T')[0]; onPopulationFound(populationValue, today); } } - + // Reset sliders to defaults - elements.simplifySlider.value = 5; // Maps to ~0.001 tolerance - elements.simplifyValue.textContent = formatTolerance(sliderToTolerance(5)); + elements.simplifySlider.value = 5; // Maps to ~0.001 tolerance + elements.simplifyValue.textContent = formatTolerance( + sliderToTolerance(5) + ); elements.bufferSlider.value = 0.1; elements.bufferValue.textContent = '0.1'; - + // Reset mega simplify controls if (elements.megaSimplifyCheckbox) { elements.megaSimplifyCheckbox.checked = false; @@ -231,10 +241,10 @@ function initOsmGeojsonGenerator(options = {}) { if (elements.tightnessValue) { elements.tightnessValue.textContent = 'Loose'; } - + // Show controls and process elements.controls.style.display = 'block'; - + // Show original boundary and process updateOriginalBoundaryLayer(); processAndPreviewGeojson(); @@ -243,7 +253,9 @@ function initOsmGeojsonGenerator(options = {}) { // Confirmation dialog for replacing existing polygon function confirmPolygonReplacement() { return new Promise((resolve) => { - const result = confirm('Replace existing polygon with the selected OSM boundary?'); + const result = confirm( + 'Replace existing polygon with the selected OSM boundary?' + ); resolve(result); }); } @@ -251,23 +263,30 @@ function initOsmGeojsonGenerator(options = {}) { // Show/hide original boundary reference layer function updateOriginalBoundaryLayer() { const map = mapEditor.map; - + // Remove existing layer if (originalBoundaryLayer) { map.removeLayer(originalBoundaryLayer); originalBoundaryLayer = null; } - + if (!originalOsmGeojson || !elements.showOriginalBoundary.checked) { return; } - + // Wrap as Feature if needed let feature = originalOsmGeojson; - if (originalOsmGeojson.type !== 'Feature' && originalOsmGeojson.type !== 'FeatureCollection') { - feature = { type: 'Feature', geometry: originalOsmGeojson, properties: {} }; + if ( + originalOsmGeojson.type !== 'Feature' && + originalOsmGeojson.type !== 'FeatureCollection' + ) { + feature = { + type: 'Feature', + geometry: originalOsmGeojson, + properties: {}, + }; } - + // Create dashed line style for reference originalBoundaryLayer = L.geoJSON(feature, { style: { @@ -275,88 +294,113 @@ function initOsmGeojsonGenerator(options = {}) { weight: 2, dashArray: '5, 5', fillOpacity: 0, - interactive: false - } + interactive: false, + }, }).addTo(map); } // Process GeoJSON with Turf.js and preview function processAndPreviewGeojson() { if (!originalOsmGeojson) return; - + const map = mapEditor.map; - + try { // Wrap as Feature if needed let feature = originalOsmGeojson; - if (originalOsmGeojson.type !== 'Feature' && originalOsmGeojson.type !== 'FeatureCollection') { - feature = { type: 'Feature', geometry: originalOsmGeojson, properties: {} }; + if ( + originalOsmGeojson.type !== 'Feature' && + originalOsmGeojson.type !== 'FeatureCollection' + ) { + feature = { + type: 'Feature', + geometry: originalOsmGeojson, + properties: {}, + }; } - - const tolerance = sliderToTolerance(parseFloat(elements.simplifySlider.value)); + + const tolerance = sliderToTolerance( + parseFloat(elements.simplifySlider.value) + ); const buffer = parseFloat(elements.bufferSlider.value); - + let processed = feature; - + // Apply buffer if > 0 if (buffer > 0) { - processed = turf.buffer(processed, buffer, { units: 'kilometers' }); + processed = turf.buffer(processed, buffer, { + units: 'kilometers', + }); } - + // Apply simplification if > 0 if (tolerance > 0) { processed = turf.simplify(processed, { tolerance: tolerance, - highQuality: true + highQuality: true, }); } - + // Apply mega simplify if checked - if (elements.megaSimplifyCheckbox && elements.megaSimplifyCheckbox.checked) { - const concavity = sliderToConcavity(parseFloat(elements.tightnessSlider.value)); - + if ( + elements.megaSimplifyCheckbox && + elements.megaSimplifyCheckbox.checked + ) { + const concavity = sliderToConcavity( + parseFloat(elements.tightnessSlider.value) + ); + try { // Use turf.convex with concavity parameter // concavity: 1 = tight, Infinity = pure convex hull - processed = turf.convex(processed, { concavity: concavity }); - + processed = turf.convex(processed, { + concavity: concavity, + }); + if (!processed) { - console.warn('[OsmGeojsonGenerator] Convex hull returned null'); + console.warn( + '[OsmGeojsonGenerator] Convex hull returned null' + ); // Fallback: try pure convex processed = turf.convex(feature); } } catch (e) { - console.warn('[OsmGeojsonGenerator] Convex hull failed:', e.message); + console.warn( + '[OsmGeojsonGenerator] Convex hull failed:', + e.message + ); // Keep original processed value } } - + processedGeojson = processed; - + // Remove existing preview layer if (previewLayer) { map.removeLayer(previewLayer); } - + // Add preview layer previewLayer = L.geoJSON(processed, { style: { color: '#3388ff', weight: 3, fillColor: '#3388ff', - fillOpacity: 0.2 - } + fillOpacity: 0.2, + }, }).addTo(map); - + // Fit bounds map.fitBounds(previewLayer.getBounds()); - + // Update point count const count = countGeojsonPoints(processed); elements.pointsCount.textContent = `Points: ${count}`; - } catch (error) { - console.error('[OsmGeojsonGenerator] Error processing GeoJSON:', error); + console.error( + '[OsmGeojsonGenerator] Error processing GeoJSON:', + error + ); showError(`Error processing: ${error.message}`); } } @@ -365,44 +409,53 @@ function initOsmGeojsonGenerator(options = {}) { function applyProcessedGeojson() { if (!processedGeojson) { if (typeof showToast === 'function') { - showToast('Warning', 'No processed GeoJSON to apply', 'warning'); + showToast( + 'Warning', + 'No processed GeoJSON to apply', + 'warning' + ); } return; } - + const map = mapEditor.map; - + // Extract geometry const geometry = processedGeojson.geometry || processedGeojson; - + // Remove preview layer if (previewLayer) { map.removeLayer(previewLayer); previewLayer = null; } - + // Remove original boundary layer if (originalBoundaryLayer) { map.removeLayer(originalBoundaryLayer); originalBoundaryLayer = null; } - + // Call the onApply callback if (onApply) { onApply(geometry); } - + // Hide controls and reset state elements.controls.style.display = 'none'; elements.resultsContainer.style.display = 'none'; elements.searchInput.value = ''; - elements.resultsSelect.innerHTML = ''; + elements.resultsSelect.innerHTML = + ''; originalOsmGeojson = null; processedGeojson = null; osmSearchResults = []; - + if (typeof showToast === 'function') { - showToast('Success', 'GeoJSON applied. You can now edit the polygon on the map.', 'success'); + showToast( + 'Success', + 'GeoJSON applied. You can now edit the polygon on the map.', + 'success' + ); } } @@ -410,7 +463,7 @@ function initOsmGeojsonGenerator(options = {}) { if (elements.searchBtn) { elements.searchBtn.addEventListener('click', searchOSM); } - + if (elements.searchInput) { elements.searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { @@ -419,33 +472,36 @@ function initOsmGeojsonGenerator(options = {}) { } }); } - + if (elements.resultsSelect) { elements.resultsSelect.addEventListener('change', onSearchResultSelect); } - + if (elements.simplifySlider) { - elements.simplifySlider.addEventListener('input', function() { + elements.simplifySlider.addEventListener('input', function () { const tolerance = sliderToTolerance(parseFloat(this.value)); elements.simplifyValue.textContent = formatTolerance(tolerance); processAndPreviewGeojson(); }); } - + if (elements.bufferSlider) { - elements.bufferSlider.addEventListener('input', function() { + elements.bufferSlider.addEventListener('input', function () { elements.bufferValue.textContent = this.value; processAndPreviewGeojson(); }); } - + if (elements.showOriginalBoundary) { - elements.showOriginalBoundary.addEventListener('change', updateOriginalBoundaryLayer); + elements.showOriginalBoundary.addEventListener( + 'change', + updateOriginalBoundaryLayer + ); } - + // Mega simplify checkbox if (elements.megaSimplifyCheckbox) { - elements.megaSimplifyCheckbox.addEventListener('change', function() { + elements.megaSimplifyCheckbox.addEventListener('change', function () { // Enable/disable tightness slider based on checkbox if (elements.tightnessSlider) { elements.tightnessSlider.disabled = !this.checked; @@ -453,10 +509,10 @@ function initOsmGeojsonGenerator(options = {}) { processAndPreviewGeojson(); }); } - + // Tightness slider if (elements.tightnessSlider) { - elements.tightnessSlider.addEventListener('input', function() { + elements.tightnessSlider.addEventListener('input', function () { const val = parseInt(this.value); if (elements.tightnessValue) { elements.tightnessValue.textContent = formatTightness(val); @@ -464,7 +520,7 @@ function initOsmGeojsonGenerator(options = {}) { processAndPreviewGeojson(); }); } - + if (elements.applyBtn) { elements.applyBtn.addEventListener('click', applyProcessedGeojson); } @@ -488,7 +544,7 @@ function initOsmGeojsonGenerator(options = {}) { originalOsmGeojson = null; processedGeojson = null; osmSearchResults = []; - } + }, }; } diff --git a/static/js/script.js b/static/js/script.js index ef4af9e..9e49504 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1,7 +1,7 @@ /** * Centralized API fetch wrapper that handles session expiration gracefully. * When the session expires, shows a toast notification and redirects to login. - * + * * @param {string} url - The URL to fetch * @param {Object} options - Fetch options (method, headers, body, etc.) * @returns {Promise} - The fetch response @@ -9,15 +9,21 @@ */ async function apiFetch(url, options = {}) { const response = await fetch(url, options); - + // Check for 401 status (session expired) if (response.status === 401) { try { const data = await response.clone().json(); if (data.session_expired) { - showToast('Session Expired', 'Your session has expired. Redirecting to login...', 'warning'); + showToast( + 'Session Expired', + 'Your session has expired. Redirecting to login...', + 'warning' + ); setTimeout(() => { - window.location.href = '/login?next=' + encodeURIComponent(window.location.href); + window.location.href = + '/login?next=' + + encodeURIComponent(window.location.href); }, 1500); throw new Error('Session expired'); } @@ -29,24 +35,35 @@ async function apiFetch(url, options = {}) { // For other parse errors, check if we got redirected to login page const text = await response.clone().text(); if (text.includes('Login') || response.url.includes('/login')) { - showToast('Session Expired', 'Your session has expired. Redirecting to login...', 'warning'); + showToast( + 'Session Expired', + 'Your session has expired. Redirecting to login...', + 'warning' + ); setTimeout(() => { - window.location.href = '/login?next=' + encodeURIComponent(window.location.href); + window.location.href = + '/login?next=' + + encodeURIComponent(window.location.href); }, 1500); throw new Error('Session expired'); } } } - + // Also check if we were redirected to login page (for cases where 401 isn't returned) if (response.redirected && response.url.includes('/login')) { - showToast('Session Expired', 'Your session has expired. Redirecting to login...', 'warning'); + showToast( + 'Session Expired', + 'Your session has expired. Redirecting to login...', + 'warning' + ); setTimeout(() => { - window.location.href = '/login?next=' + encodeURIComponent(window.location.href); + window.location.href = + '/login?next=' + encodeURIComponent(window.location.href); }, 1500); throw new Error('Session expired'); } - + return response; } @@ -58,9 +75,9 @@ function editTag(areaId, tagName, tagValue) { } function addTag(areaId) { - const tagName = prompt("Enter tag name:"); + const tagName = prompt('Enter tag name:'); if (tagName) { - const tagValue = prompt("Enter tag value:"); + const tagValue = prompt('Enter tag value:'); if (tagValue) { setAreaTag(areaId, tagName, tagValue); } @@ -75,19 +92,23 @@ function setAreaTag(areaId, tagName, tagValue) { }, body: JSON.stringify({ id: areaId, name: tagName, value: tagValue }), }) - .then(response => response.json()) - .then(data => { - if (data.error) { - showToast('Error', data.error.message || 'Failed to update tag', 'error'); - } else { - location.reload(); - } - }) - .catch(error => { - if (error.message !== 'Session expired') { - showToast('Error', 'Failed to update tag', 'error'); - } - }); + .then((response) => response.json()) + .then((data) => { + if (data.error) { + showToast( + 'Error', + data.error.message || 'Failed to update tag', + 'error' + ); + } else { + location.reload(); + } + }) + .catch((error) => { + if (error.message !== 'Session expired') { + showToast('Error', 'Failed to update tag', 'error'); + } + }); } function removeTag(areaId, tagName) { @@ -98,20 +119,24 @@ function removeTag(areaId, tagName) { }, body: JSON.stringify({ id: areaId, tag: tagName }), }) - .then(response => response.json()) - .then(data => { - if (data.error) { - showToast('Error', data.error.message || 'Failed to remove tag', 'error'); - } else { - showToast('Success', 'Tag removed successfully', 'success'); - setTimeout(() => location.reload(), 1000); - } - }) - .catch(error => { - if (error.message !== 'Session expired') { - showToast('Error', 'Failed to remove tag', 'error'); - } - }); + .then((response) => response.json()) + .then((data) => { + if (data.error) { + showToast( + 'Error', + data.error.message || 'Failed to remove tag', + 'error' + ); + } else { + showToast('Success', 'Tag removed successfully', 'success'); + setTimeout(() => location.reload(), 1000); + } + }) + .catch((error) => { + if (error.message !== 'Session expired') { + showToast('Error', 'Failed to remove tag', 'error'); + } + }); } function removeArea(areaId) { @@ -122,18 +147,22 @@ function removeArea(areaId) { }, body: JSON.stringify({ id: areaId }), }) - .then(response => response.json()) - .then(data => { - if (data.error) { - showToast('Error', data.error.message || 'Failed to remove area', 'error'); - } else { - showToast('Success', 'Area removed successfully', 'success'); - setTimeout(() => window.location.href = '/select_area', 1500); - } - }) - .catch(error => { - if (error.message !== 'Session expired') { - showToast('Error', 'Failed to remove area', 'error'); - } - }); + .then((response) => response.json()) + .then((data) => { + if (data.error) { + showToast( + 'Error', + data.error.message || 'Failed to remove area', + 'error' + ); + } else { + showToast('Success', 'Area removed successfully', 'success'); + setTimeout(() => (window.location.href = '/select_area'), 1500); + } + }) + .catch((error) => { + if (error.message !== 'Session expired') { + showToast('Error', 'Failed to remove area', 'error'); + } + }); } diff --git a/static/js/validation.js b/static/js/validation.js index f7a0dae..94cf602 100644 --- a/static/js/validation.js +++ b/static/js/validation.js @@ -8,7 +8,11 @@ function validateKey(key, existingKeys) { return { isValid: false, message: 'Key already exists' }; } if (!/^[a-zA-Z][a-zA-Z0-9_:]*$/.test(key)) { - return { isValid: false, message: 'Key must start with a letter and contain only letters, numbers, underscores, and colons' }; + return { + isValid: false, + message: + 'Key must start with a letter and contain only letters, numbers, underscores, and colons', + }; } return { isValid: true }; } @@ -19,10 +23,13 @@ function validateNumericValue(value, type) { } value = value.toString().trim(); - + if (type === 'integer') { if (!/^\d+$/.test(value)) { - return { isValid: false, message: 'Value must be a valid integer (no decimal points)' }; + return { + isValid: false, + message: 'Value must be a valid integer (no decimal points)', + }; } const num = parseInt(value, 10); if (num < 0) { @@ -31,7 +38,11 @@ function validateNumericValue(value, type) { return { isValid: true, value: num }; } else if (type === 'number') { if (!/^\d*\.?\d*$/.test(value)) { - return { isValid: false, message: 'Value must contain only digits and at most one decimal point' }; + return { + isValid: false, + message: + 'Value must contain only digits and at most one decimal point', + }; } const num = parseFloat(value); if (isNaN(num)) { @@ -56,7 +67,10 @@ function validateValue(value, requirements) { return validateNumericValue(value, requirements.type); } else if (requirements.allowed_values) { if (!requirements.allowed_values.includes(value)) { - return { isValid: false, message: `Value must be one of: ${requirements.allowed_values.join(', ')}` }; + return { + isValid: false, + message: `Value must be one of: ${requirements.allowed_values.join(', ')}`, + }; } } } From 22a05f420d29e66458bb85d2b31b6ed17ea506dc Mon Sep 17 00:00:00 2001 From: escapedcat Date: Sat, 21 Feb 2026 12:32:55 +0100 Subject: [PATCH 2/5] style: simplify biome.json config --- biome.json | 78 +- package.json | 24 +- static/css/map.css | 6 +- static/css/style.css | 68 +- static/js/map-editor.js | 1396 ++++++++++++++-------------- static/js/osm-geojson-generator.js | 1056 ++++++++++----------- static/js/script.js | 278 +++--- static/js/validation.js | 130 +-- 8 files changed, 1514 insertions(+), 1522 deletions(-) diff --git a/biome.json b/biome.json index 19a097d..572a471 100644 --- a/biome.json +++ b/biome.json @@ -1,45 +1,37 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "complexity": { - "noForEach": "off", - "useOptionalChain": "off", - "useArrowFunction": "off" - }, - "style": { - "noParameterAssign": "off", - "noUselessElse": "off", - "useNumberNamespace": "off", - "useTemplate": "off", - "useExponentiationOperator": "off" - }, - "suspicious": { - "noAssignInExpressions": "off", - "noGlobalIsNan": "off" - } - }, - "ignore": ["**/.venv/**", "**/node_modules/**", "**/__pycache__/**"] - }, - "formatter": { - "enabled": true, - "formatWithErrors": false, - "indentStyle": "space", - "indentWidth": 4, - "lineWidth": 80, - "ignore": ["**/.venv/**", "**/node_modules/**", "**/__pycache__/**"] - }, - "javascript": { - "formatter": { - "quoteStyle": "single", - "trailingCommas": "es5", - "semicolons": "always", - "indentWidth": 4 - } - } + "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "rules": { + "recommended": true, + "complexity": { + "noForEach": "off", + "useOptionalChain": "off", + "useArrowFunction": "off" + }, + "style": { + "noParameterAssign": "off", + "noUselessElse": "off", + "useNumberNamespace": "off", + "useTemplate": "off", + "useExponentiationOperator": "off" + }, + "suspicious": { + "noAssignInExpressions": "off", + "noGlobalIsNan": "off" + } + }, + "ignore": ["**/.venv/**", "**/node_modules/**", "**/__pycache__/**"] + }, + "formatter": { + "indentWidth": 4, + "ignore": ["**/.venv/**", "**/node_modules/**", "**/__pycache__/**"] + }, + "javascript": { + "formatter": { + "quoteStyle": "single" + } + } } diff --git a/package.json b/package.json index b04d742..20b53ff 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { - "name": "btcmap-admin", - "version": "0.1.0", - "description": "BTC Map Admin - Frontend dev tools", - "private": true, - "scripts": { - "lint": "biome check .", - "format": "biome format --write .", - "lint:fix": "biome check --write ." - }, - "devDependencies": { - "@biomejs/biome": "^1.9.0" - } + "name": "btcmap-admin", + "version": "0.1.0", + "description": "BTC Map Admin - Frontend dev tools", + "private": true, + "scripts": { + "lint": "biome check .", + "format": "biome format --write .", + "lint:fix": "biome check --write ." + }, + "devDependencies": { + "@biomejs/biome": "^1.9.0" + } } diff --git a/static/css/map.css b/static/css/map.css index a23f0de..2574f2a 100644 --- a/static/css/map.css +++ b/static/css/map.css @@ -1,5 +1,5 @@ #map { - height: 400px; - width: 100%; - margin-bottom: 20px; + height: 400px; + width: 100%; + margin-bottom: 20px; } diff --git a/static/css/style.css b/static/css/style.css index db9b71b..90fc3f3 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,80 +1,80 @@ body { - padding-top: 5rem; + padding-top: 5rem; } .starter-template { - padding: 3rem 1.5rem; - text-align: center; + padding: 3rem 1.5rem; + text-align: center; } .form-signin { - width: 100%; - max-width: 330px; - padding: 15px; - margin: auto; + width: 100%; + max-width: 330px; + padding: 15px; + margin: auto; } .form-signin .form-control { - position: relative; - box-sizing: border-box; - height: auto; - padding: 10px; - font-size: 16px; + position: relative; + box-sizing: border-box; + height: auto; + padding: 10px; + font-size: 16px; } .form-signin .form-control:focus { - z-index: 2; + z-index: 2; } .form-signin input[type="password"] { - margin-bottom: 10px; - border-top-left-radius: 0; - border-top-right-radius: 0; + margin-bottom: 10px; + border-top-left-radius: 0; + border-top-right-radius: 0; } /* Area details table - prevent field value overflow */ #tags-table { - table-layout: fixed; - width: 100%; + table-layout: fixed; + width: 100%; } #tags-table th:first-child, #tags-table td:first-child { - width: 15%; + width: 15%; } #tags-table th:nth-child(2), #tags-table td:nth-child(2) { - width: 65%; + width: 65%; } #tags-table th:last-child, #tags-table td:last-child { - width: 20%; - white-space: nowrap; + width: 20%; + white-space: nowrap; } /* Action buttons - standalone with spacing */ #tags-table .btn-group { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; + display: flex; + flex-wrap: wrap; + gap: 0.25rem; } #tags-table .btn-group .btn { - border-radius: 0.25rem !important; + border-radius: 0.25rem !important; } #tags-table .tag-value-cell { - overflow-wrap: break-word; - word-wrap: break-word; - word-break: break-word; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; } #tags-table .tag-value-content { - display: block; - overflow-wrap: break-word; - word-wrap: break-word; - word-break: break-word; - white-space: normal; + display: block; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + white-space: normal; } diff --git a/static/js/map-editor.js b/static/js/map-editor.js index 5f255fa..a631328 100644 --- a/static/js/map-editor.js +++ b/static/js/map-editor.js @@ -25,706 +25,706 @@ */ function initMapEditor(options = {}) { - const { - containerId = 'map', - initialGeoJson = null, - onGeoJsonChange = null, - } = options; - - // State - let currentGeoJson = null; - let geoJsonLayer = null; - let previouslySavedGeoJson = null; - - // Shape editor state - let originalShapeGeoJson = null; // Original shape before editing - let shapePreviewLayer = null; // Preview layer for shape edits - let originalShapeLayer = null; // Reference layer showing original - - // DOM Elements - const elements = { - mapContainer: document.getElementById(containerId), - // Raw GeoJSON editor - editGeoJsonBtn: document.getElementById('edit-geojson-btn'), - geoJsonEditor: document.getElementById('geojson-editor'), - geoJsonInput: document.getElementById('geojson-input'), - showBtn: document.getElementById('show-geojson-btn'), - saveLocallyBtn: document.getElementById('save-locally-btn'), - cancelBtn: document.getElementById('cancel-geojson-btn'), - // Shape editor - editShapeBtn: document.getElementById('edit-shape-btn'), - shapeEditor: document.getElementById('shape-editor'), - shapeSimplifySlider: document.getElementById('shape-simplify-slider'), - shapeSimplifyValue: document.getElementById('shape-simplify-value'), - shapeBufferSlider: document.getElementById('shape-buffer-slider'), - shapeBufferValue: document.getElementById('shape-buffer-value'), - shapeMegaSimplifyCheckbox: document.getElementById( - 'shape-mega-simplify' - ), - shapeTightnessSlider: document.getElementById('shape-tightness-slider'), - shapeTightnessValue: document.getElementById('shape-tightness-value'), - shapePointsCount: document.getElementById('shape-points-count'), - shapeShowOriginal: document.getElementById('shape-show-original'), - applyShapeBtn: document.getElementById('apply-shape-btn'), - cancelShapeBtn: document.getElementById('cancel-shape-btn'), - }; - - // Initialize map - if (!elements.mapContainer) { - console.error('[MapEditor] Map container not found:', containerId); - return null; - } - - const map = L.map(containerId).setView([0, 0], 2); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors', - }).addTo(map); - - // Initialize draw control - const drawnItems = new L.FeatureGroup(); - map.addLayer(drawnItems); - - const drawControl = new L.Control.Draw({ - draw: { - polygon: { - allowIntersection: false, - showArea: true, - shapeOptions: { - color: '#3388ff', - weight: 3, - }, - }, - rectangle: { - shapeOptions: { - color: '#3388ff', - weight: 3, - }, - }, - circle: false, - circlemarker: false, - marker: false, - polyline: false, - }, - edit: { - featureGroup: drawnItems, - remove: true, - edit: true, - }, - }); - map.addControl(drawControl); - - // Update GeoJSON from drawn items - function updateGeoJsonFromDrawnItems() { - if (drawnItems.getLayers().length === 0) { - currentGeoJson = null; - if (elements.geoJsonInput) { - elements.geoJsonInput.value = ''; - } - if (onGeoJsonChange) { - onGeoJsonChange(null); - } - return; - } - - const layer = drawnItems.getLayers()[0]; - const geoJson = layer.toGeoJSON(); - currentGeoJson = geoJson.geometry; - - // Keep textarea in sync - if (elements.geoJsonInput) { - elements.geoJsonInput.value = JSON.stringify( - currentGeoJson, - null, - 2 - ); - } - - map.fitBounds(drawnItems.getBounds()); - - if (onGeoJsonChange) { - onGeoJsonChange(currentGeoJson); - } - } - - // Update map with GeoJSON - function updateGeoJson(geoJson) { - try { - // Remove existing geoJsonLayer - if (geoJsonLayer) { - map.removeLayer(geoJsonLayer); - } - - // Clear drawn items - drawnItems.clearLayers(); - - if (!geoJson) { - currentGeoJson = null; - if (elements.geoJsonInput) { - elements.geoJsonInput.value = ''; - } - if (onGeoJsonChange) { - onGeoJsonChange(null); - } - return true; - } - - // Handle string input - if (typeof geoJson === 'string') { - geoJson = JSON.parse(geoJson); - } - - // Extract geometry if wrapped in Feature - let geometry = geoJson; - if (geoJson.type === 'Feature') { - geometry = geoJson.geometry; - } else if ( - geoJson.type === 'FeatureCollection' && - geoJson.features && - geoJson.features.length > 0 - ) { - geometry = geoJson.features[0].geometry; - } - - // Store just the geometry - currentGeoJson = geometry; - - // Wrap back as Feature for display in Leaflet - const featureCollection = { - type: 'Feature', - geometry: geometry, - }; - - // Create GeoJSON feature and add to map - geoJsonLayer = L.geoJSON(featureCollection).addTo(map); - map.fitBounds(geoJsonLayer.getBounds()); - - // Also add to drawnItems for editing - if (geoJsonLayer.getLayers().length > 0) { - const layer = geoJsonLayer.getLayers()[0]; - drawnItems.addLayer(layer); - } - - // Update textarea with current GeoJSON (keep them in sync) - if (elements.geoJsonInput) { - elements.geoJsonInput.value = JSON.stringify( - currentGeoJson, - null, - 2 - ); - } - - if (onGeoJsonChange) { - onGeoJsonChange(currentGeoJson); - } - - return true; - } catch (error) { - console.error('[MapEditor] Error adding GeoJSON to map:', error); - if (typeof showToast === 'function') { - showToast( - 'Error', - `Invalid GeoJSON: ${error.message}`, - 'error' - ); - } - return false; - } - } - - // Handle draw created - map.on(L.Draw.Event.CREATED, function (e) { - const layer = e.layer; - // Clear existing and add new - drawnItems.clearLayers(); - drawnItems.addLayer(layer); - updateGeoJsonFromDrawnItems(); - }); - - // Handle draw edited - map.on(L.Draw.Event.EDITED, function (e) { - updateGeoJsonFromDrawnItems(); - }); - - // Handle draw deleted - map.on(L.Draw.Event.DELETED, function (e) { - currentGeoJson = null; - if (geoJsonLayer) { - map.removeLayer(geoJsonLayer); - geoJsonLayer = null; - } - if (elements.geoJsonInput) { - elements.geoJsonInput.value = ''; - } - if (onGeoJsonChange) { - onGeoJsonChange(null); - } - }); - - // Raw GeoJSON editor functionality - function showEditor() { - if (elements.geoJsonEditor) { - elements.geoJsonEditor.style.display = 'block'; - } - if (currentGeoJson && elements.geoJsonInput) { - elements.geoJsonInput.value = JSON.stringify( - currentGeoJson, - null, - 2 - ); - previouslySavedGeoJson = JSON.parse(JSON.stringify(currentGeoJson)); - } - } - - function hideEditor() { - if (elements.geoJsonEditor) { - elements.geoJsonEditor.style.display = 'none'; - } - } - - function showGeoJsonPreview() { - try { - const geoJson = JSON.parse(elements.geoJsonInput.value); - if (updateGeoJson(geoJson)) { - if (typeof showToast === 'function') { - showToast('Success', 'GeoJSON preview updated', 'success'); - } - } - } catch (error) { - console.error('[MapEditor] Error parsing GeoJSON:', error); - if (typeof showToast === 'function') { - showToast( - 'Error', - 'Invalid GeoJSON: ' + error.message, - 'error' - ); - } - } - } - - function saveLocally() { - try { - const geoJson = JSON.parse(elements.geoJsonInput.value); - if (updateGeoJson(geoJson)) { - hideEditor(); - previouslySavedGeoJson = currentGeoJson; - if (typeof showToast === 'function') { - showToast('Success', 'GeoJSON saved locally', 'success'); - } - } - } catch (error) { - console.error('[MapEditor] Error saving GeoJSON:', error); - if (typeof showToast === 'function') { - showToast( - 'Error', - 'Invalid GeoJSON: ' + error.message, - 'error' - ); - } - } - } - - function cancelEditing() { - hideEditor(); - if (previouslySavedGeoJson) { - updateGeoJson(previouslySavedGeoJson); - } - } - - // ============================================ - // Shape Editor functionality - // ============================================ - - // Convert linear slider (0-10) to logarithmic tolerance - function sliderToTolerance(sliderVal) { - if (sliderVal === 0) return 0; - const minLog = Math.log10(0.000001); - const maxLog = Math.log10(0.01); - const logVal = minLog + (sliderVal / 10) * (maxLog - minLog); - return Math.pow(10, logVal); - } - - function formatTolerance(val) { - if (val === 0) return '0'; - if (val < 0.00001) return val.toExponential(1); - if (val < 0.0001) return val.toFixed(6); - if (val < 0.001) return val.toFixed(5); - if (val < 0.01) return val.toFixed(4); - return val.toFixed(3); - } - - // Convert tightness slider (0-10) to concavity for turf.convex() - // 0 = pure convex (concavity = Infinity) - // 10 = tighter fit (concavity = 1) - function sliderToConcavity(sliderVal) { - if (sliderVal === 0) return Infinity; - return Math.max(1, 21 - sliderVal * 2); - } - - function formatTightness(val) { - if (val === 0) return 'Loose'; - if (val === 10) return 'Tight'; - return val.toString(); - } - - function countGeojsonPoints(geojson) { - let count = 0; - function countCoords(coords) { - if (typeof coords[0] === 'number') { - count++; - } else { - coords.forEach((c) => countCoords(c)); - } - } - const geometry = geojson.geometry || geojson; - if (geometry && geometry.coordinates) { - countCoords(geometry.coordinates); - } - return count; - } - - function showShapeEditor() { - if (!currentGeoJson) { - if (typeof showToast === 'function') { - showToast( - 'Warning', - 'No shape to edit. Draw or import a polygon first.', - 'warning' - ); - } - return; - } - - // Store original shape - originalShapeGeoJson = JSON.parse(JSON.stringify(currentGeoJson)); - - // Hide raw GeoJSON editor if open - hideEditor(); - - // Reset sliders to neutral (no changes) - if (elements.shapeSimplifySlider) { - elements.shapeSimplifySlider.value = 0; - elements.shapeSimplifyValue.textContent = '0'; - } - if (elements.shapeBufferSlider) { - elements.shapeBufferSlider.value = 0; - elements.shapeBufferValue.textContent = '0'; - } - if (elements.shapeMegaSimplifyCheckbox) { - elements.shapeMegaSimplifyCheckbox.checked = false; - } - if (elements.shapeTightnessSlider) { - elements.shapeTightnessSlider.value = 0; - elements.shapeTightnessSlider.disabled = true; - elements.shapeTightnessValue.textContent = 'Loose'; - } - - // Show editor - if (elements.shapeEditor) { - elements.shapeEditor.style.display = 'block'; - } - - // Show original shape and initial preview - updateOriginalShapeLayer(); - processAndPreviewShape(); - } - - function hideShapeEditor() { - if (elements.shapeEditor) { - elements.shapeEditor.style.display = 'none'; - } - // Remove preview layers - if (shapePreviewLayer) { - map.removeLayer(shapePreviewLayer); - shapePreviewLayer = null; - } - if (originalShapeLayer) { - map.removeLayer(originalShapeLayer); - originalShapeLayer = null; - } - } - - function updateOriginalShapeLayer() { - // Remove existing layer - if (originalShapeLayer) { - map.removeLayer(originalShapeLayer); - originalShapeLayer = null; - } - - if ( - !originalShapeGeoJson || - !elements.shapeShowOriginal || - !elements.shapeShowOriginal.checked - ) { - return; - } - - // Wrap as Feature if needed - let feature = originalShapeGeoJson; - if ( - originalShapeGeoJson.type !== 'Feature' && - originalShapeGeoJson.type !== 'FeatureCollection' - ) { - feature = { - type: 'Feature', - geometry: originalShapeGeoJson, - properties: {}, - }; - } - - // Create dashed line style for reference - originalShapeLayer = L.geoJSON(feature, { - style: { - color: '#ff6600', - weight: 2, - dashArray: '5, 5', - fillOpacity: 0, - interactive: false, - }, - }).addTo(map); - } - - function processAndPreviewShape() { - if (!originalShapeGeoJson) return; - - try { - // Wrap as Feature if needed - let feature = originalShapeGeoJson; - if ( - originalShapeGeoJson.type !== 'Feature' && - originalShapeGeoJson.type !== 'FeatureCollection' - ) { - feature = { - type: 'Feature', - geometry: originalShapeGeoJson, - properties: {}, - }; - } - - const tolerance = sliderToTolerance( - parseFloat(elements.shapeSimplifySlider?.value || 0) - ); - const buffer = parseFloat(elements.shapeBufferSlider?.value || 0); - - let processed = feature; - - // Apply buffer if > 0 - if (buffer > 0 && typeof turf !== 'undefined') { - processed = turf.buffer(processed, buffer, { - units: 'kilometers', - }); - } - - // Apply simplification if > 0 - if (tolerance > 0 && typeof turf !== 'undefined') { - processed = turf.simplify(processed, { - tolerance: tolerance, - highQuality: true, - }); - } - - // Apply mega simplify if checked - if ( - elements.shapeMegaSimplifyCheckbox?.checked && - typeof turf !== 'undefined' - ) { - const concavity = sliderToConcavity( - parseFloat(elements.shapeTightnessSlider?.value || 0) - ); - - try { - // Use turf.convex with concavity parameter - processed = turf.convex(processed, { - concavity: concavity, - }); - - if (!processed) { - console.warn('[MapEditor] Convex hull returned null'); - processed = turf.convex(feature); - } - } catch (e) { - console.warn('[MapEditor] Convex hull failed:', e.message); - // Keep original processed value - } - } - - // Remove existing preview layer - if (shapePreviewLayer) { - map.removeLayer(shapePreviewLayer); - } - - // Hide the main drawn items while previewing - drawnItems.eachLayer((layer) => { - layer.setStyle({ opacity: 0, fillOpacity: 0 }); - }); - - // Add preview layer - shapePreviewLayer = L.geoJSON(processed, { - style: { - color: '#3388ff', - weight: 3, - fillColor: '#3388ff', - fillOpacity: 0.2, - }, - }).addTo(map); - - // Fit bounds - map.fitBounds(shapePreviewLayer.getBounds()); - - // Update point count - const count = countGeojsonPoints(processed); - if (elements.shapePointsCount) { - elements.shapePointsCount.textContent = `Points: ${count}`; - } - } catch (error) { - console.error('[MapEditor] Error processing shape:', error); - if (typeof showToast === 'function') { - showToast( - 'Error', - `Error processing: ${error.message}`, - 'error' - ); - } - } - } - - function applyShapeChanges() { - if (!shapePreviewLayer) { - if (typeof showToast === 'function') { - showToast('Warning', 'No changes to apply', 'warning'); - } - return; - } - - // Get geometry from preview layer - const previewGeoJson = shapePreviewLayer.toGeoJSON(); - let geometry = previewGeoJson; - if ( - previewGeoJson.type === 'FeatureCollection' && - previewGeoJson.features.length > 0 - ) { - geometry = previewGeoJson.features[0].geometry; - } else if (previewGeoJson.type === 'Feature') { - geometry = previewGeoJson.geometry; - } - - // Hide shape editor - hideShapeEditor(); - - // Restore visibility of drawn items - drawnItems.eachLayer((layer) => { - layer.setStyle({ opacity: 1, fillOpacity: 0.2 }); - }); - - // Apply the processed geometry - updateGeoJson(geometry); - - if (typeof showToast === 'function') { - showToast('Success', 'Shape changes applied', 'success'); - } - } - - function cancelShapeEditing() { - hideShapeEditor(); - - // Restore visibility of drawn items - drawnItems.eachLayer((layer) => { - layer.setStyle({ opacity: 1, fillOpacity: 0.2 }); - }); - - // Restore original shape - if (originalShapeGeoJson) { - updateGeoJson(originalShapeGeoJson); - } - originalShapeGeoJson = null; - } - - // Set up event listeners - Raw GeoJSON editor - if (elements.editGeoJsonBtn) { - elements.editGeoJsonBtn.addEventListener('click', showEditor); - } - if (elements.showBtn) { - elements.showBtn.addEventListener('click', showGeoJsonPreview); - } - if (elements.saveLocallyBtn) { - elements.saveLocallyBtn.addEventListener('click', saveLocally); - } - if (elements.cancelBtn) { - elements.cancelBtn.addEventListener('click', cancelEditing); - } - - // Set up event listeners - Shape editor - if (elements.editShapeBtn) { - elements.editShapeBtn.addEventListener('click', showShapeEditor); - } - if (elements.shapeSimplifySlider) { - elements.shapeSimplifySlider.addEventListener('input', function () { - const tolerance = sliderToTolerance(parseFloat(this.value)); - elements.shapeSimplifyValue.textContent = - formatTolerance(tolerance); - processAndPreviewShape(); - }); - } - if (elements.shapeBufferSlider) { - elements.shapeBufferSlider.addEventListener('input', function () { - elements.shapeBufferValue.textContent = this.value; - processAndPreviewShape(); - }); - } - if (elements.shapeMegaSimplifyCheckbox) { - elements.shapeMegaSimplifyCheckbox.addEventListener( - 'change', - function () { - if (elements.shapeTightnessSlider) { - elements.shapeTightnessSlider.disabled = !this.checked; - } - processAndPreviewShape(); - } - ); - } - if (elements.shapeTightnessSlider) { - elements.shapeTightnessSlider.addEventListener('input', function () { - const val = parseInt(this.value); - if (elements.shapeTightnessValue) { - elements.shapeTightnessValue.textContent = formatTightness(val); - } - processAndPreviewShape(); - }); - } - if (elements.shapeShowOriginal) { - elements.shapeShowOriginal.addEventListener( - 'change', - updateOriginalShapeLayer - ); - } - if (elements.applyShapeBtn) { - elements.applyShapeBtn.addEventListener('click', applyShapeChanges); - } - if (elements.cancelShapeBtn) { - elements.cancelShapeBtn.addEventListener('click', cancelShapeEditing); - } - - // Load initial GeoJSON if provided - if (initialGeoJson) { - // Delay slightly to ensure map is fully initialized - setTimeout(() => { - updateGeoJson(initialGeoJson); - }, 100); - } - - // Return public API - return { - map, - drawnItems, - getCurrentGeoJson: () => currentGeoJson, - updateGeoJson, - showEditor, - hideEditor, - showShapeEditor, - hideShapeEditor, - }; + const { + containerId = 'map', + initialGeoJson = null, + onGeoJsonChange = null, + } = options; + + // State + let currentGeoJson = null; + let geoJsonLayer = null; + let previouslySavedGeoJson = null; + + // Shape editor state + let originalShapeGeoJson = null; // Original shape before editing + let shapePreviewLayer = null; // Preview layer for shape edits + let originalShapeLayer = null; // Reference layer showing original + + // DOM Elements + const elements = { + mapContainer: document.getElementById(containerId), + // Raw GeoJSON editor + editGeoJsonBtn: document.getElementById('edit-geojson-btn'), + geoJsonEditor: document.getElementById('geojson-editor'), + geoJsonInput: document.getElementById('geojson-input'), + showBtn: document.getElementById('show-geojson-btn'), + saveLocallyBtn: document.getElementById('save-locally-btn'), + cancelBtn: document.getElementById('cancel-geojson-btn'), + // Shape editor + editShapeBtn: document.getElementById('edit-shape-btn'), + shapeEditor: document.getElementById('shape-editor'), + shapeSimplifySlider: document.getElementById('shape-simplify-slider'), + shapeSimplifyValue: document.getElementById('shape-simplify-value'), + shapeBufferSlider: document.getElementById('shape-buffer-slider'), + shapeBufferValue: document.getElementById('shape-buffer-value'), + shapeMegaSimplifyCheckbox: document.getElementById( + 'shape-mega-simplify', + ), + shapeTightnessSlider: document.getElementById('shape-tightness-slider'), + shapeTightnessValue: document.getElementById('shape-tightness-value'), + shapePointsCount: document.getElementById('shape-points-count'), + shapeShowOriginal: document.getElementById('shape-show-original'), + applyShapeBtn: document.getElementById('apply-shape-btn'), + cancelShapeBtn: document.getElementById('cancel-shape-btn'), + }; + + // Initialize map + if (!elements.mapContainer) { + console.error('[MapEditor] Map container not found:', containerId); + return null; + } + + const map = L.map(containerId).setView([0, 0], 2); + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + }).addTo(map); + + // Initialize draw control + const drawnItems = new L.FeatureGroup(); + map.addLayer(drawnItems); + + const drawControl = new L.Control.Draw({ + draw: { + polygon: { + allowIntersection: false, + showArea: true, + shapeOptions: { + color: '#3388ff', + weight: 3, + }, + }, + rectangle: { + shapeOptions: { + color: '#3388ff', + weight: 3, + }, + }, + circle: false, + circlemarker: false, + marker: false, + polyline: false, + }, + edit: { + featureGroup: drawnItems, + remove: true, + edit: true, + }, + }); + map.addControl(drawControl); + + // Update GeoJSON from drawn items + function updateGeoJsonFromDrawnItems() { + if (drawnItems.getLayers().length === 0) { + currentGeoJson = null; + if (elements.geoJsonInput) { + elements.geoJsonInput.value = ''; + } + if (onGeoJsonChange) { + onGeoJsonChange(null); + } + return; + } + + const layer = drawnItems.getLayers()[0]; + const geoJson = layer.toGeoJSON(); + currentGeoJson = geoJson.geometry; + + // Keep textarea in sync + if (elements.geoJsonInput) { + elements.geoJsonInput.value = JSON.stringify( + currentGeoJson, + null, + 2, + ); + } + + map.fitBounds(drawnItems.getBounds()); + + if (onGeoJsonChange) { + onGeoJsonChange(currentGeoJson); + } + } + + // Update map with GeoJSON + function updateGeoJson(geoJson) { + try { + // Remove existing geoJsonLayer + if (geoJsonLayer) { + map.removeLayer(geoJsonLayer); + } + + // Clear drawn items + drawnItems.clearLayers(); + + if (!geoJson) { + currentGeoJson = null; + if (elements.geoJsonInput) { + elements.geoJsonInput.value = ''; + } + if (onGeoJsonChange) { + onGeoJsonChange(null); + } + return true; + } + + // Handle string input + if (typeof geoJson === 'string') { + geoJson = JSON.parse(geoJson); + } + + // Extract geometry if wrapped in Feature + let geometry = geoJson; + if (geoJson.type === 'Feature') { + geometry = geoJson.geometry; + } else if ( + geoJson.type === 'FeatureCollection' && + geoJson.features && + geoJson.features.length > 0 + ) { + geometry = geoJson.features[0].geometry; + } + + // Store just the geometry + currentGeoJson = geometry; + + // Wrap back as Feature for display in Leaflet + const featureCollection = { + type: 'Feature', + geometry: geometry, + }; + + // Create GeoJSON feature and add to map + geoJsonLayer = L.geoJSON(featureCollection).addTo(map); + map.fitBounds(geoJsonLayer.getBounds()); + + // Also add to drawnItems for editing + if (geoJsonLayer.getLayers().length > 0) { + const layer = geoJsonLayer.getLayers()[0]; + drawnItems.addLayer(layer); + } + + // Update textarea with current GeoJSON (keep them in sync) + if (elements.geoJsonInput) { + elements.geoJsonInput.value = JSON.stringify( + currentGeoJson, + null, + 2, + ); + } + + if (onGeoJsonChange) { + onGeoJsonChange(currentGeoJson); + } + + return true; + } catch (error) { + console.error('[MapEditor] Error adding GeoJSON to map:', error); + if (typeof showToast === 'function') { + showToast( + 'Error', + `Invalid GeoJSON: ${error.message}`, + 'error', + ); + } + return false; + } + } + + // Handle draw created + map.on(L.Draw.Event.CREATED, function (e) { + const layer = e.layer; + // Clear existing and add new + drawnItems.clearLayers(); + drawnItems.addLayer(layer); + updateGeoJsonFromDrawnItems(); + }); + + // Handle draw edited + map.on(L.Draw.Event.EDITED, function (e) { + updateGeoJsonFromDrawnItems(); + }); + + // Handle draw deleted + map.on(L.Draw.Event.DELETED, function (e) { + currentGeoJson = null; + if (geoJsonLayer) { + map.removeLayer(geoJsonLayer); + geoJsonLayer = null; + } + if (elements.geoJsonInput) { + elements.geoJsonInput.value = ''; + } + if (onGeoJsonChange) { + onGeoJsonChange(null); + } + }); + + // Raw GeoJSON editor functionality + function showEditor() { + if (elements.geoJsonEditor) { + elements.geoJsonEditor.style.display = 'block'; + } + if (currentGeoJson && elements.geoJsonInput) { + elements.geoJsonInput.value = JSON.stringify( + currentGeoJson, + null, + 2, + ); + previouslySavedGeoJson = JSON.parse(JSON.stringify(currentGeoJson)); + } + } + + function hideEditor() { + if (elements.geoJsonEditor) { + elements.geoJsonEditor.style.display = 'none'; + } + } + + function showGeoJsonPreview() { + try { + const geoJson = JSON.parse(elements.geoJsonInput.value); + if (updateGeoJson(geoJson)) { + if (typeof showToast === 'function') { + showToast('Success', 'GeoJSON preview updated', 'success'); + } + } + } catch (error) { + console.error('[MapEditor] Error parsing GeoJSON:', error); + if (typeof showToast === 'function') { + showToast( + 'Error', + 'Invalid GeoJSON: ' + error.message, + 'error', + ); + } + } + } + + function saveLocally() { + try { + const geoJson = JSON.parse(elements.geoJsonInput.value); + if (updateGeoJson(geoJson)) { + hideEditor(); + previouslySavedGeoJson = currentGeoJson; + if (typeof showToast === 'function') { + showToast('Success', 'GeoJSON saved locally', 'success'); + } + } + } catch (error) { + console.error('[MapEditor] Error saving GeoJSON:', error); + if (typeof showToast === 'function') { + showToast( + 'Error', + 'Invalid GeoJSON: ' + error.message, + 'error', + ); + } + } + } + + function cancelEditing() { + hideEditor(); + if (previouslySavedGeoJson) { + updateGeoJson(previouslySavedGeoJson); + } + } + + // ============================================ + // Shape Editor functionality + // ============================================ + + // Convert linear slider (0-10) to logarithmic tolerance + function sliderToTolerance(sliderVal) { + if (sliderVal === 0) return 0; + const minLog = Math.log10(0.000001); + const maxLog = Math.log10(0.01); + const logVal = minLog + (sliderVal / 10) * (maxLog - minLog); + return Math.pow(10, logVal); + } + + function formatTolerance(val) { + if (val === 0) return '0'; + if (val < 0.00001) return val.toExponential(1); + if (val < 0.0001) return val.toFixed(6); + if (val < 0.001) return val.toFixed(5); + if (val < 0.01) return val.toFixed(4); + return val.toFixed(3); + } + + // Convert tightness slider (0-10) to concavity for turf.convex() + // 0 = pure convex (concavity = Infinity) + // 10 = tighter fit (concavity = 1) + function sliderToConcavity(sliderVal) { + if (sliderVal === 0) return Infinity; + return Math.max(1, 21 - sliderVal * 2); + } + + function formatTightness(val) { + if (val === 0) return 'Loose'; + if (val === 10) return 'Tight'; + return val.toString(); + } + + function countGeojsonPoints(geojson) { + let count = 0; + function countCoords(coords) { + if (typeof coords[0] === 'number') { + count++; + } else { + coords.forEach((c) => countCoords(c)); + } + } + const geometry = geojson.geometry || geojson; + if (geometry && geometry.coordinates) { + countCoords(geometry.coordinates); + } + return count; + } + + function showShapeEditor() { + if (!currentGeoJson) { + if (typeof showToast === 'function') { + showToast( + 'Warning', + 'No shape to edit. Draw or import a polygon first.', + 'warning', + ); + } + return; + } + + // Store original shape + originalShapeGeoJson = JSON.parse(JSON.stringify(currentGeoJson)); + + // Hide raw GeoJSON editor if open + hideEditor(); + + // Reset sliders to neutral (no changes) + if (elements.shapeSimplifySlider) { + elements.shapeSimplifySlider.value = 0; + elements.shapeSimplifyValue.textContent = '0'; + } + if (elements.shapeBufferSlider) { + elements.shapeBufferSlider.value = 0; + elements.shapeBufferValue.textContent = '0'; + } + if (elements.shapeMegaSimplifyCheckbox) { + elements.shapeMegaSimplifyCheckbox.checked = false; + } + if (elements.shapeTightnessSlider) { + elements.shapeTightnessSlider.value = 0; + elements.shapeTightnessSlider.disabled = true; + elements.shapeTightnessValue.textContent = 'Loose'; + } + + // Show editor + if (elements.shapeEditor) { + elements.shapeEditor.style.display = 'block'; + } + + // Show original shape and initial preview + updateOriginalShapeLayer(); + processAndPreviewShape(); + } + + function hideShapeEditor() { + if (elements.shapeEditor) { + elements.shapeEditor.style.display = 'none'; + } + // Remove preview layers + if (shapePreviewLayer) { + map.removeLayer(shapePreviewLayer); + shapePreviewLayer = null; + } + if (originalShapeLayer) { + map.removeLayer(originalShapeLayer); + originalShapeLayer = null; + } + } + + function updateOriginalShapeLayer() { + // Remove existing layer + if (originalShapeLayer) { + map.removeLayer(originalShapeLayer); + originalShapeLayer = null; + } + + if ( + !originalShapeGeoJson || + !elements.shapeShowOriginal || + !elements.shapeShowOriginal.checked + ) { + return; + } + + // Wrap as Feature if needed + let feature = originalShapeGeoJson; + if ( + originalShapeGeoJson.type !== 'Feature' && + originalShapeGeoJson.type !== 'FeatureCollection' + ) { + feature = { + type: 'Feature', + geometry: originalShapeGeoJson, + properties: {}, + }; + } + + // Create dashed line style for reference + originalShapeLayer = L.geoJSON(feature, { + style: { + color: '#ff6600', + weight: 2, + dashArray: '5, 5', + fillOpacity: 0, + interactive: false, + }, + }).addTo(map); + } + + function processAndPreviewShape() { + if (!originalShapeGeoJson) return; + + try { + // Wrap as Feature if needed + let feature = originalShapeGeoJson; + if ( + originalShapeGeoJson.type !== 'Feature' && + originalShapeGeoJson.type !== 'FeatureCollection' + ) { + feature = { + type: 'Feature', + geometry: originalShapeGeoJson, + properties: {}, + }; + } + + const tolerance = sliderToTolerance( + parseFloat(elements.shapeSimplifySlider?.value || 0), + ); + const buffer = parseFloat(elements.shapeBufferSlider?.value || 0); + + let processed = feature; + + // Apply buffer if > 0 + if (buffer > 0 && typeof turf !== 'undefined') { + processed = turf.buffer(processed, buffer, { + units: 'kilometers', + }); + } + + // Apply simplification if > 0 + if (tolerance > 0 && typeof turf !== 'undefined') { + processed = turf.simplify(processed, { + tolerance: tolerance, + highQuality: true, + }); + } + + // Apply mega simplify if checked + if ( + elements.shapeMegaSimplifyCheckbox?.checked && + typeof turf !== 'undefined' + ) { + const concavity = sliderToConcavity( + parseFloat(elements.shapeTightnessSlider?.value || 0), + ); + + try { + // Use turf.convex with concavity parameter + processed = turf.convex(processed, { + concavity: concavity, + }); + + if (!processed) { + console.warn('[MapEditor] Convex hull returned null'); + processed = turf.convex(feature); + } + } catch (e) { + console.warn('[MapEditor] Convex hull failed:', e.message); + // Keep original processed value + } + } + + // Remove existing preview layer + if (shapePreviewLayer) { + map.removeLayer(shapePreviewLayer); + } + + // Hide the main drawn items while previewing + drawnItems.eachLayer((layer) => { + layer.setStyle({ opacity: 0, fillOpacity: 0 }); + }); + + // Add preview layer + shapePreviewLayer = L.geoJSON(processed, { + style: { + color: '#3388ff', + weight: 3, + fillColor: '#3388ff', + fillOpacity: 0.2, + }, + }).addTo(map); + + // Fit bounds + map.fitBounds(shapePreviewLayer.getBounds()); + + // Update point count + const count = countGeojsonPoints(processed); + if (elements.shapePointsCount) { + elements.shapePointsCount.textContent = `Points: ${count}`; + } + } catch (error) { + console.error('[MapEditor] Error processing shape:', error); + if (typeof showToast === 'function') { + showToast( + 'Error', + `Error processing: ${error.message}`, + 'error', + ); + } + } + } + + function applyShapeChanges() { + if (!shapePreviewLayer) { + if (typeof showToast === 'function') { + showToast('Warning', 'No changes to apply', 'warning'); + } + return; + } + + // Get geometry from preview layer + const previewGeoJson = shapePreviewLayer.toGeoJSON(); + let geometry = previewGeoJson; + if ( + previewGeoJson.type === 'FeatureCollection' && + previewGeoJson.features.length > 0 + ) { + geometry = previewGeoJson.features[0].geometry; + } else if (previewGeoJson.type === 'Feature') { + geometry = previewGeoJson.geometry; + } + + // Hide shape editor + hideShapeEditor(); + + // Restore visibility of drawn items + drawnItems.eachLayer((layer) => { + layer.setStyle({ opacity: 1, fillOpacity: 0.2 }); + }); + + // Apply the processed geometry + updateGeoJson(geometry); + + if (typeof showToast === 'function') { + showToast('Success', 'Shape changes applied', 'success'); + } + } + + function cancelShapeEditing() { + hideShapeEditor(); + + // Restore visibility of drawn items + drawnItems.eachLayer((layer) => { + layer.setStyle({ opacity: 1, fillOpacity: 0.2 }); + }); + + // Restore original shape + if (originalShapeGeoJson) { + updateGeoJson(originalShapeGeoJson); + } + originalShapeGeoJson = null; + } + + // Set up event listeners - Raw GeoJSON editor + if (elements.editGeoJsonBtn) { + elements.editGeoJsonBtn.addEventListener('click', showEditor); + } + if (elements.showBtn) { + elements.showBtn.addEventListener('click', showGeoJsonPreview); + } + if (elements.saveLocallyBtn) { + elements.saveLocallyBtn.addEventListener('click', saveLocally); + } + if (elements.cancelBtn) { + elements.cancelBtn.addEventListener('click', cancelEditing); + } + + // Set up event listeners - Shape editor + if (elements.editShapeBtn) { + elements.editShapeBtn.addEventListener('click', showShapeEditor); + } + if (elements.shapeSimplifySlider) { + elements.shapeSimplifySlider.addEventListener('input', function () { + const tolerance = sliderToTolerance(parseFloat(this.value)); + elements.shapeSimplifyValue.textContent = + formatTolerance(tolerance); + processAndPreviewShape(); + }); + } + if (elements.shapeBufferSlider) { + elements.shapeBufferSlider.addEventListener('input', function () { + elements.shapeBufferValue.textContent = this.value; + processAndPreviewShape(); + }); + } + if (elements.shapeMegaSimplifyCheckbox) { + elements.shapeMegaSimplifyCheckbox.addEventListener( + 'change', + function () { + if (elements.shapeTightnessSlider) { + elements.shapeTightnessSlider.disabled = !this.checked; + } + processAndPreviewShape(); + }, + ); + } + if (elements.shapeTightnessSlider) { + elements.shapeTightnessSlider.addEventListener('input', function () { + const val = parseInt(this.value); + if (elements.shapeTightnessValue) { + elements.shapeTightnessValue.textContent = formatTightness(val); + } + processAndPreviewShape(); + }); + } + if (elements.shapeShowOriginal) { + elements.shapeShowOriginal.addEventListener( + 'change', + updateOriginalShapeLayer, + ); + } + if (elements.applyShapeBtn) { + elements.applyShapeBtn.addEventListener('click', applyShapeChanges); + } + if (elements.cancelShapeBtn) { + elements.cancelShapeBtn.addEventListener('click', cancelShapeEditing); + } + + // Load initial GeoJSON if provided + if (initialGeoJson) { + // Delay slightly to ensure map is fully initialized + setTimeout(() => { + updateGeoJson(initialGeoJson); + }, 100); + } + + // Return public API + return { + map, + drawnItems, + getCurrentGeoJson: () => currentGeoJson, + updateGeoJson, + showEditor, + hideEditor, + showShapeEditor, + hideShapeEditor, + }; } // Export for module systems if available if (typeof module !== 'undefined' && module.exports) { - module.exports = { initMapEditor }; + module.exports = { initMapEditor }; } diff --git a/static/js/osm-geojson-generator.js b/static/js/osm-geojson-generator.js index 5b66481..0745204 100644 --- a/static/js/osm-geojson-generator.js +++ b/static/js/osm-geojson-generator.js @@ -19,536 +19,536 @@ */ function initOsmGeojsonGenerator(options = {}) { - const { - mapEditor = null, - onApply = null, - onPopulationFound = null, - } = options; - - if (!mapEditor) { - console.error('[OsmGeojsonGenerator] mapEditor is required'); - return null; - } - - // DOM Elements - const elements = { - searchInput: document.getElementById('osm-search-input'), - searchBtn: document.getElementById('osm-search-btn'), - resultsContainer: document.getElementById('search-results-container'), - resultsSelect: document.getElementById('search-results'), - controls: document.getElementById('geojson-controls'), - simplifySlider: document.getElementById('simplify-slider'), - simplifyValue: document.getElementById('simplify-value'), - bufferSlider: document.getElementById('buffer-slider'), - bufferValue: document.getElementById('buffer-value'), - megaSimplifyCheckbox: document.getElementById('mega-simplify'), - tightnessSlider: document.getElementById('tightness-slider'), - tightnessValue: document.getElementById('tightness-value'), - pointsCount: document.getElementById('points-count'), - showOriginalBoundary: document.getElementById('show-original-boundary'), - applyBtn: document.getElementById('apply-geojson-btn'), - loading: document.getElementById('geojson-loading'), - error: document.getElementById('geojson-error'), - }; - - // State - let originalOsmGeojson = null; // Full-detail from Nominatim - let processedGeojson = null; // After simplification/buffer - let originalBoundaryLayer = null; // Reference layer showing original - let previewLayer = null; // Preview layer for processed result - let osmSearchResults = []; // Store search results - - // Helper functions - function showError(message) { - if (elements.error) { - elements.error.textContent = message; - elements.error.style.display = 'block'; - } - } - - function hideError() { - if (elements.error) { - elements.error.style.display = 'none'; - } - } - - function showLoading(show) { - if (elements.loading) { - elements.loading.style.display = show ? 'block' : 'none'; - } - } - - function countGeojsonPoints(geojson) { - let count = 0; - - function countCoords(coords) { - if (typeof coords[0] === 'number') { - count++; - } else { - coords.forEach((c) => countCoords(c)); - } - } - - const geometry = geojson.geometry || geojson; - if (geometry && geometry.coordinates) { - countCoords(geometry.coordinates); - } - - return count; - } - - // Convert linear slider (0-10) to logarithmic tolerance - // 0 = 0 (no simplification), 1 = 0.000001, 5 = 0.0001, 10 = 0.01 - function sliderToTolerance(sliderVal) { - if (sliderVal === 0) return 0; - // Map 1-10 to 0.000001-0.01 logarithmically (6 orders of magnitude) - const minLog = Math.log10(0.000001); // -6 - const maxLog = Math.log10(0.01); // -2 - const logVal = minLog + (sliderVal / 10) * (maxLog - minLog); - return Math.pow(10, logVal); - } - - function formatTolerance(val) { - if (val === 0) return '0'; - if (val < 0.00001) return val.toExponential(1); - if (val < 0.0001) return val.toFixed(6); - if (val < 0.001) return val.toFixed(5); - if (val < 0.01) return val.toFixed(4); - return val.toFixed(3); - } - - // Convert tightness slider (0-10) to concavity for turf.convex() - // 0 = pure convex (concavity = Infinity) - // 10 = tighter fit (concavity = 1) - function sliderToConcavity(sliderVal) { - if (sliderVal === 0) return Infinity; // Pure convex hull - // Map 1-10 to concavity values (higher = looser, lower = tighter) - // 1 = 20 (loose), 10 = 1 (tight) - return Math.max(1, 21 - sliderVal * 2); - } - - function formatTightness(val) { - if (val === 0) return 'Loose'; - if (val === 10) return 'Tight'; - return val.toString(); - } - - // Search OSM via Nominatim - async function searchOSM() { - const query = elements.searchInput.value.trim(); - if (!query) { - if (typeof showToast === 'function') { - showToast('Warning', 'Please enter a search term', 'warning'); - } - return; - } - - hideError(); - elements.resultsContainer.style.display = 'none'; - elements.controls.style.display = 'none'; - showLoading(true); - - try { - const response = await apiFetch( - `/api/search_osm?q=${encodeURIComponent(query)}` - ); - const results = await response.json(); - - if (results.error) { - throw new Error(results.error); - } - - if (results.length === 0) { - showError( - 'No administrative areas found. Try a different search term.' - ); - return; - } - - // Store results and populate dropdown - osmSearchResults = results; - elements.resultsSelect.innerHTML = - ''; - results.forEach((r, index) => { - const option = document.createElement('option'); - option.value = index; - option.textContent = r.display_name; - elements.resultsSelect.appendChild(option); - }); - - elements.resultsContainer.style.display = 'block'; - } catch (error) { - console.error('[OsmGeojsonGenerator] Search error:', error); - showError(error.message || 'Search failed'); - } finally { - showLoading(false); - } - } - - // Handle search result selection - async function onSearchResultSelect() { - const index = elements.resultsSelect.value; - if (index === '') return; - - const result = osmSearchResults[parseInt(index)]; - if (!result || !result.geojson) { - showError('Selected place has no boundary data'); - return; - } - - // Check if there's already a polygon, confirm before replacing - const currentGeoJson = mapEditor.getCurrentGeoJson(); - if (currentGeoJson && mapEditor.drawnItems.getLayers().length > 0) { - const confirmed = await confirmPolygonReplacement(); - if (!confirmed) { - elements.resultsSelect.value = ''; - return; - } - } - - // Store the original GeoJSON - originalOsmGeojson = result.geojson; - - // Handle population data from OSM extratags - if ( - result.extratags && - result.extratags.population && - onPopulationFound - ) { - const populationValue = parseInt(result.extratags.population, 10); - if (!isNaN(populationValue)) { - const today = new Date().toISOString().split('T')[0]; - onPopulationFound(populationValue, today); - } - } - - // Reset sliders to defaults - elements.simplifySlider.value = 5; // Maps to ~0.001 tolerance - elements.simplifyValue.textContent = formatTolerance( - sliderToTolerance(5) - ); - elements.bufferSlider.value = 0.1; - elements.bufferValue.textContent = '0.1'; - - // Reset mega simplify controls - if (elements.megaSimplifyCheckbox) { - elements.megaSimplifyCheckbox.checked = false; - } - if (elements.tightnessSlider) { - elements.tightnessSlider.value = 0; - elements.tightnessSlider.disabled = true; - } - if (elements.tightnessValue) { - elements.tightnessValue.textContent = 'Loose'; - } - - // Show controls and process - elements.controls.style.display = 'block'; - - // Show original boundary and process - updateOriginalBoundaryLayer(); - processAndPreviewGeojson(); - } - - // Confirmation dialog for replacing existing polygon - function confirmPolygonReplacement() { - return new Promise((resolve) => { - const result = confirm( - 'Replace existing polygon with the selected OSM boundary?' - ); - resolve(result); - }); - } - - // Show/hide original boundary reference layer - function updateOriginalBoundaryLayer() { - const map = mapEditor.map; - - // Remove existing layer - if (originalBoundaryLayer) { - map.removeLayer(originalBoundaryLayer); - originalBoundaryLayer = null; - } - - if (!originalOsmGeojson || !elements.showOriginalBoundary.checked) { - return; - } - - // Wrap as Feature if needed - let feature = originalOsmGeojson; - if ( - originalOsmGeojson.type !== 'Feature' && - originalOsmGeojson.type !== 'FeatureCollection' - ) { - feature = { - type: 'Feature', - geometry: originalOsmGeojson, - properties: {}, - }; - } - - // Create dashed line style for reference - originalBoundaryLayer = L.geoJSON(feature, { - style: { - color: '#ff6600', - weight: 2, - dashArray: '5, 5', - fillOpacity: 0, - interactive: false, - }, - }).addTo(map); - } - - // Process GeoJSON with Turf.js and preview - function processAndPreviewGeojson() { - if (!originalOsmGeojson) return; - - const map = mapEditor.map; - - try { - // Wrap as Feature if needed - let feature = originalOsmGeojson; - if ( - originalOsmGeojson.type !== 'Feature' && - originalOsmGeojson.type !== 'FeatureCollection' - ) { - feature = { - type: 'Feature', - geometry: originalOsmGeojson, - properties: {}, - }; - } - - const tolerance = sliderToTolerance( - parseFloat(elements.simplifySlider.value) - ); - const buffer = parseFloat(elements.bufferSlider.value); - - let processed = feature; - - // Apply buffer if > 0 - if (buffer > 0) { - processed = turf.buffer(processed, buffer, { - units: 'kilometers', - }); - } - - // Apply simplification if > 0 - if (tolerance > 0) { - processed = turf.simplify(processed, { - tolerance: tolerance, - highQuality: true, - }); - } - - // Apply mega simplify if checked - if ( - elements.megaSimplifyCheckbox && - elements.megaSimplifyCheckbox.checked - ) { - const concavity = sliderToConcavity( - parseFloat(elements.tightnessSlider.value) - ); - - try { - // Use turf.convex with concavity parameter - // concavity: 1 = tight, Infinity = pure convex hull - processed = turf.convex(processed, { - concavity: concavity, - }); - - if (!processed) { - console.warn( - '[OsmGeojsonGenerator] Convex hull returned null' - ); - // Fallback: try pure convex - processed = turf.convex(feature); - } - } catch (e) { - console.warn( - '[OsmGeojsonGenerator] Convex hull failed:', - e.message - ); - // Keep original processed value - } - } - - processedGeojson = processed; - - // Remove existing preview layer - if (previewLayer) { - map.removeLayer(previewLayer); - } - - // Add preview layer - previewLayer = L.geoJSON(processed, { - style: { - color: '#3388ff', - weight: 3, - fillColor: '#3388ff', - fillOpacity: 0.2, - }, - }).addTo(map); - - // Fit bounds - map.fitBounds(previewLayer.getBounds()); - - // Update point count - const count = countGeojsonPoints(processed); - elements.pointsCount.textContent = `Points: ${count}`; - } catch (error) { - console.error( - '[OsmGeojsonGenerator] Error processing GeoJSON:', - error - ); - showError(`Error processing: ${error.message}`); - } - } - - // Apply the processed GeoJSON to the map for editing - function applyProcessedGeojson() { - if (!processedGeojson) { - if (typeof showToast === 'function') { - showToast( - 'Warning', - 'No processed GeoJSON to apply', - 'warning' - ); - } - return; - } - - const map = mapEditor.map; - - // Extract geometry - const geometry = processedGeojson.geometry || processedGeojson; - - // Remove preview layer - if (previewLayer) { - map.removeLayer(previewLayer); - previewLayer = null; - } - - // Remove original boundary layer - if (originalBoundaryLayer) { - map.removeLayer(originalBoundaryLayer); - originalBoundaryLayer = null; - } - - // Call the onApply callback - if (onApply) { - onApply(geometry); - } - - // Hide controls and reset state - elements.controls.style.display = 'none'; - elements.resultsContainer.style.display = 'none'; - elements.searchInput.value = ''; - elements.resultsSelect.innerHTML = - ''; - originalOsmGeojson = null; - processedGeojson = null; - osmSearchResults = []; - - if (typeof showToast === 'function') { - showToast( - 'Success', - 'GeoJSON applied. You can now edit the polygon on the map.', - 'success' - ); - } - } - - // Set up event listeners - if (elements.searchBtn) { - elements.searchBtn.addEventListener('click', searchOSM); - } - - if (elements.searchInput) { - elements.searchInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - searchOSM(); - } - }); - } - - if (elements.resultsSelect) { - elements.resultsSelect.addEventListener('change', onSearchResultSelect); - } - - if (elements.simplifySlider) { - elements.simplifySlider.addEventListener('input', function () { - const tolerance = sliderToTolerance(parseFloat(this.value)); - elements.simplifyValue.textContent = formatTolerance(tolerance); - processAndPreviewGeojson(); - }); - } - - if (elements.bufferSlider) { - elements.bufferSlider.addEventListener('input', function () { - elements.bufferValue.textContent = this.value; - processAndPreviewGeojson(); - }); - } - - if (elements.showOriginalBoundary) { - elements.showOriginalBoundary.addEventListener( - 'change', - updateOriginalBoundaryLayer - ); - } - - // Mega simplify checkbox - if (elements.megaSimplifyCheckbox) { - elements.megaSimplifyCheckbox.addEventListener('change', function () { - // Enable/disable tightness slider based on checkbox - if (elements.tightnessSlider) { - elements.tightnessSlider.disabled = !this.checked; - } - processAndPreviewGeojson(); - }); - } - - // Tightness slider - if (elements.tightnessSlider) { - elements.tightnessSlider.addEventListener('input', function () { - const val = parseInt(this.value); - if (elements.tightnessValue) { - elements.tightnessValue.textContent = formatTightness(val); - } - processAndPreviewGeojson(); - }); - } - - if (elements.applyBtn) { - elements.applyBtn.addEventListener('click', applyProcessedGeojson); - } - - // Return public API (minimal, mostly self-contained) - return { - searchOSM, - reset: () => { - const map = mapEditor.map; - if (previewLayer) { - map.removeLayer(previewLayer); - previewLayer = null; - } - if (originalBoundaryLayer) { - map.removeLayer(originalBoundaryLayer); - originalBoundaryLayer = null; - } - elements.controls.style.display = 'none'; - elements.resultsContainer.style.display = 'none'; - elements.searchInput.value = ''; - originalOsmGeojson = null; - processedGeojson = null; - osmSearchResults = []; - }, - }; + const { + mapEditor = null, + onApply = null, + onPopulationFound = null, + } = options; + + if (!mapEditor) { + console.error('[OsmGeojsonGenerator] mapEditor is required'); + return null; + } + + // DOM Elements + const elements = { + searchInput: document.getElementById('osm-search-input'), + searchBtn: document.getElementById('osm-search-btn'), + resultsContainer: document.getElementById('search-results-container'), + resultsSelect: document.getElementById('search-results'), + controls: document.getElementById('geojson-controls'), + simplifySlider: document.getElementById('simplify-slider'), + simplifyValue: document.getElementById('simplify-value'), + bufferSlider: document.getElementById('buffer-slider'), + bufferValue: document.getElementById('buffer-value'), + megaSimplifyCheckbox: document.getElementById('mega-simplify'), + tightnessSlider: document.getElementById('tightness-slider'), + tightnessValue: document.getElementById('tightness-value'), + pointsCount: document.getElementById('points-count'), + showOriginalBoundary: document.getElementById('show-original-boundary'), + applyBtn: document.getElementById('apply-geojson-btn'), + loading: document.getElementById('geojson-loading'), + error: document.getElementById('geojson-error'), + }; + + // State + let originalOsmGeojson = null; // Full-detail from Nominatim + let processedGeojson = null; // After simplification/buffer + let originalBoundaryLayer = null; // Reference layer showing original + let previewLayer = null; // Preview layer for processed result + let osmSearchResults = []; // Store search results + + // Helper functions + function showError(message) { + if (elements.error) { + elements.error.textContent = message; + elements.error.style.display = 'block'; + } + } + + function hideError() { + if (elements.error) { + elements.error.style.display = 'none'; + } + } + + function showLoading(show) { + if (elements.loading) { + elements.loading.style.display = show ? 'block' : 'none'; + } + } + + function countGeojsonPoints(geojson) { + let count = 0; + + function countCoords(coords) { + if (typeof coords[0] === 'number') { + count++; + } else { + coords.forEach((c) => countCoords(c)); + } + } + + const geometry = geojson.geometry || geojson; + if (geometry && geometry.coordinates) { + countCoords(geometry.coordinates); + } + + return count; + } + + // Convert linear slider (0-10) to logarithmic tolerance + // 0 = 0 (no simplification), 1 = 0.000001, 5 = 0.0001, 10 = 0.01 + function sliderToTolerance(sliderVal) { + if (sliderVal === 0) return 0; + // Map 1-10 to 0.000001-0.01 logarithmically (6 orders of magnitude) + const minLog = Math.log10(0.000001); // -6 + const maxLog = Math.log10(0.01); // -2 + const logVal = minLog + (sliderVal / 10) * (maxLog - minLog); + return Math.pow(10, logVal); + } + + function formatTolerance(val) { + if (val === 0) return '0'; + if (val < 0.00001) return val.toExponential(1); + if (val < 0.0001) return val.toFixed(6); + if (val < 0.001) return val.toFixed(5); + if (val < 0.01) return val.toFixed(4); + return val.toFixed(3); + } + + // Convert tightness slider (0-10) to concavity for turf.convex() + // 0 = pure convex (concavity = Infinity) + // 10 = tighter fit (concavity = 1) + function sliderToConcavity(sliderVal) { + if (sliderVal === 0) return Infinity; // Pure convex hull + // Map 1-10 to concavity values (higher = looser, lower = tighter) + // 1 = 20 (loose), 10 = 1 (tight) + return Math.max(1, 21 - sliderVal * 2); + } + + function formatTightness(val) { + if (val === 0) return 'Loose'; + if (val === 10) return 'Tight'; + return val.toString(); + } + + // Search OSM via Nominatim + async function searchOSM() { + const query = elements.searchInput.value.trim(); + if (!query) { + if (typeof showToast === 'function') { + showToast('Warning', 'Please enter a search term', 'warning'); + } + return; + } + + hideError(); + elements.resultsContainer.style.display = 'none'; + elements.controls.style.display = 'none'; + showLoading(true); + + try { + const response = await apiFetch( + `/api/search_osm?q=${encodeURIComponent(query)}`, + ); + const results = await response.json(); + + if (results.error) { + throw new Error(results.error); + } + + if (results.length === 0) { + showError( + 'No administrative areas found. Try a different search term.', + ); + return; + } + + // Store results and populate dropdown + osmSearchResults = results; + elements.resultsSelect.innerHTML = + ''; + results.forEach((r, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = r.display_name; + elements.resultsSelect.appendChild(option); + }); + + elements.resultsContainer.style.display = 'block'; + } catch (error) { + console.error('[OsmGeojsonGenerator] Search error:', error); + showError(error.message || 'Search failed'); + } finally { + showLoading(false); + } + } + + // Handle search result selection + async function onSearchResultSelect() { + const index = elements.resultsSelect.value; + if (index === '') return; + + const result = osmSearchResults[parseInt(index)]; + if (!result || !result.geojson) { + showError('Selected place has no boundary data'); + return; + } + + // Check if there's already a polygon, confirm before replacing + const currentGeoJson = mapEditor.getCurrentGeoJson(); + if (currentGeoJson && mapEditor.drawnItems.getLayers().length > 0) { + const confirmed = await confirmPolygonReplacement(); + if (!confirmed) { + elements.resultsSelect.value = ''; + return; + } + } + + // Store the original GeoJSON + originalOsmGeojson = result.geojson; + + // Handle population data from OSM extratags + if ( + result.extratags && + result.extratags.population && + onPopulationFound + ) { + const populationValue = parseInt(result.extratags.population, 10); + if (!isNaN(populationValue)) { + const today = new Date().toISOString().split('T')[0]; + onPopulationFound(populationValue, today); + } + } + + // Reset sliders to defaults + elements.simplifySlider.value = 5; // Maps to ~0.001 tolerance + elements.simplifyValue.textContent = formatTolerance( + sliderToTolerance(5), + ); + elements.bufferSlider.value = 0.1; + elements.bufferValue.textContent = '0.1'; + + // Reset mega simplify controls + if (elements.megaSimplifyCheckbox) { + elements.megaSimplifyCheckbox.checked = false; + } + if (elements.tightnessSlider) { + elements.tightnessSlider.value = 0; + elements.tightnessSlider.disabled = true; + } + if (elements.tightnessValue) { + elements.tightnessValue.textContent = 'Loose'; + } + + // Show controls and process + elements.controls.style.display = 'block'; + + // Show original boundary and process + updateOriginalBoundaryLayer(); + processAndPreviewGeojson(); + } + + // Confirmation dialog for replacing existing polygon + function confirmPolygonReplacement() { + return new Promise((resolve) => { + const result = confirm( + 'Replace existing polygon with the selected OSM boundary?', + ); + resolve(result); + }); + } + + // Show/hide original boundary reference layer + function updateOriginalBoundaryLayer() { + const map = mapEditor.map; + + // Remove existing layer + if (originalBoundaryLayer) { + map.removeLayer(originalBoundaryLayer); + originalBoundaryLayer = null; + } + + if (!originalOsmGeojson || !elements.showOriginalBoundary.checked) { + return; + } + + // Wrap as Feature if needed + let feature = originalOsmGeojson; + if ( + originalOsmGeojson.type !== 'Feature' && + originalOsmGeojson.type !== 'FeatureCollection' + ) { + feature = { + type: 'Feature', + geometry: originalOsmGeojson, + properties: {}, + }; + } + + // Create dashed line style for reference + originalBoundaryLayer = L.geoJSON(feature, { + style: { + color: '#ff6600', + weight: 2, + dashArray: '5, 5', + fillOpacity: 0, + interactive: false, + }, + }).addTo(map); + } + + // Process GeoJSON with Turf.js and preview + function processAndPreviewGeojson() { + if (!originalOsmGeojson) return; + + const map = mapEditor.map; + + try { + // Wrap as Feature if needed + let feature = originalOsmGeojson; + if ( + originalOsmGeojson.type !== 'Feature' && + originalOsmGeojson.type !== 'FeatureCollection' + ) { + feature = { + type: 'Feature', + geometry: originalOsmGeojson, + properties: {}, + }; + } + + const tolerance = sliderToTolerance( + parseFloat(elements.simplifySlider.value), + ); + const buffer = parseFloat(elements.bufferSlider.value); + + let processed = feature; + + // Apply buffer if > 0 + if (buffer > 0) { + processed = turf.buffer(processed, buffer, { + units: 'kilometers', + }); + } + + // Apply simplification if > 0 + if (tolerance > 0) { + processed = turf.simplify(processed, { + tolerance: tolerance, + highQuality: true, + }); + } + + // Apply mega simplify if checked + if ( + elements.megaSimplifyCheckbox && + elements.megaSimplifyCheckbox.checked + ) { + const concavity = sliderToConcavity( + parseFloat(elements.tightnessSlider.value), + ); + + try { + // Use turf.convex with concavity parameter + // concavity: 1 = tight, Infinity = pure convex hull + processed = turf.convex(processed, { + concavity: concavity, + }); + + if (!processed) { + console.warn( + '[OsmGeojsonGenerator] Convex hull returned null', + ); + // Fallback: try pure convex + processed = turf.convex(feature); + } + } catch (e) { + console.warn( + '[OsmGeojsonGenerator] Convex hull failed:', + e.message, + ); + // Keep original processed value + } + } + + processedGeojson = processed; + + // Remove existing preview layer + if (previewLayer) { + map.removeLayer(previewLayer); + } + + // Add preview layer + previewLayer = L.geoJSON(processed, { + style: { + color: '#3388ff', + weight: 3, + fillColor: '#3388ff', + fillOpacity: 0.2, + }, + }).addTo(map); + + // Fit bounds + map.fitBounds(previewLayer.getBounds()); + + // Update point count + const count = countGeojsonPoints(processed); + elements.pointsCount.textContent = `Points: ${count}`; + } catch (error) { + console.error( + '[OsmGeojsonGenerator] Error processing GeoJSON:', + error, + ); + showError(`Error processing: ${error.message}`); + } + } + + // Apply the processed GeoJSON to the map for editing + function applyProcessedGeojson() { + if (!processedGeojson) { + if (typeof showToast === 'function') { + showToast( + 'Warning', + 'No processed GeoJSON to apply', + 'warning', + ); + } + return; + } + + const map = mapEditor.map; + + // Extract geometry + const geometry = processedGeojson.geometry || processedGeojson; + + // Remove preview layer + if (previewLayer) { + map.removeLayer(previewLayer); + previewLayer = null; + } + + // Remove original boundary layer + if (originalBoundaryLayer) { + map.removeLayer(originalBoundaryLayer); + originalBoundaryLayer = null; + } + + // Call the onApply callback + if (onApply) { + onApply(geometry); + } + + // Hide controls and reset state + elements.controls.style.display = 'none'; + elements.resultsContainer.style.display = 'none'; + elements.searchInput.value = ''; + elements.resultsSelect.innerHTML = + ''; + originalOsmGeojson = null; + processedGeojson = null; + osmSearchResults = []; + + if (typeof showToast === 'function') { + showToast( + 'Success', + 'GeoJSON applied. You can now edit the polygon on the map.', + 'success', + ); + } + } + + // Set up event listeners + if (elements.searchBtn) { + elements.searchBtn.addEventListener('click', searchOSM); + } + + if (elements.searchInput) { + elements.searchInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + searchOSM(); + } + }); + } + + if (elements.resultsSelect) { + elements.resultsSelect.addEventListener('change', onSearchResultSelect); + } + + if (elements.simplifySlider) { + elements.simplifySlider.addEventListener('input', function () { + const tolerance = sliderToTolerance(parseFloat(this.value)); + elements.simplifyValue.textContent = formatTolerance(tolerance); + processAndPreviewGeojson(); + }); + } + + if (elements.bufferSlider) { + elements.bufferSlider.addEventListener('input', function () { + elements.bufferValue.textContent = this.value; + processAndPreviewGeojson(); + }); + } + + if (elements.showOriginalBoundary) { + elements.showOriginalBoundary.addEventListener( + 'change', + updateOriginalBoundaryLayer, + ); + } + + // Mega simplify checkbox + if (elements.megaSimplifyCheckbox) { + elements.megaSimplifyCheckbox.addEventListener('change', function () { + // Enable/disable tightness slider based on checkbox + if (elements.tightnessSlider) { + elements.tightnessSlider.disabled = !this.checked; + } + processAndPreviewGeojson(); + }); + } + + // Tightness slider + if (elements.tightnessSlider) { + elements.tightnessSlider.addEventListener('input', function () { + const val = parseInt(this.value); + if (elements.tightnessValue) { + elements.tightnessValue.textContent = formatTightness(val); + } + processAndPreviewGeojson(); + }); + } + + if (elements.applyBtn) { + elements.applyBtn.addEventListener('click', applyProcessedGeojson); + } + + // Return public API (minimal, mostly self-contained) + return { + searchOSM, + reset: () => { + const map = mapEditor.map; + if (previewLayer) { + map.removeLayer(previewLayer); + previewLayer = null; + } + if (originalBoundaryLayer) { + map.removeLayer(originalBoundaryLayer); + originalBoundaryLayer = null; + } + elements.controls.style.display = 'none'; + elements.resultsContainer.style.display = 'none'; + elements.searchInput.value = ''; + originalOsmGeojson = null; + processedGeojson = null; + osmSearchResults = []; + }, + }; } // Export for module systems if available if (typeof module !== 'undefined' && module.exports) { - module.exports = { initOsmGeojsonGenerator }; + module.exports = { initOsmGeojsonGenerator }; } diff --git a/static/js/script.js b/static/js/script.js index 9e49504..0c2e078 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -8,161 +8,161 @@ * @throws {Error} - Throws an error if session expired (after redirect initiated) */ async function apiFetch(url, options = {}) { - const response = await fetch(url, options); + const response = await fetch(url, options); - // Check for 401 status (session expired) - if (response.status === 401) { - try { - const data = await response.clone().json(); - if (data.session_expired) { - showToast( - 'Session Expired', - 'Your session has expired. Redirecting to login...', - 'warning' - ); - setTimeout(() => { - window.location.href = - '/login?next=' + - encodeURIComponent(window.location.href); - }, 1500); - throw new Error('Session expired'); - } - } catch (e) { - // If we can't parse JSON or it's our own error, still handle as session expired - if (e.message === 'Session expired') { - throw e; - } - // For other parse errors, check if we got redirected to login page - const text = await response.clone().text(); - if (text.includes('Login') || response.url.includes('/login')) { - showToast( - 'Session Expired', - 'Your session has expired. Redirecting to login...', - 'warning' - ); - setTimeout(() => { - window.location.href = - '/login?next=' + - encodeURIComponent(window.location.href); - }, 1500); - throw new Error('Session expired'); - } - } - } + // Check for 401 status (session expired) + if (response.status === 401) { + try { + const data = await response.clone().json(); + if (data.session_expired) { + showToast( + 'Session Expired', + 'Your session has expired. Redirecting to login...', + 'warning', + ); + setTimeout(() => { + window.location.href = + '/login?next=' + + encodeURIComponent(window.location.href); + }, 1500); + throw new Error('Session expired'); + } + } catch (e) { + // If we can't parse JSON or it's our own error, still handle as session expired + if (e.message === 'Session expired') { + throw e; + } + // For other parse errors, check if we got redirected to login page + const text = await response.clone().text(); + if (text.includes('Login') || response.url.includes('/login')) { + showToast( + 'Session Expired', + 'Your session has expired. Redirecting to login...', + 'warning', + ); + setTimeout(() => { + window.location.href = + '/login?next=' + + encodeURIComponent(window.location.href); + }, 1500); + throw new Error('Session expired'); + } + } + } - // Also check if we were redirected to login page (for cases where 401 isn't returned) - if (response.redirected && response.url.includes('/login')) { - showToast( - 'Session Expired', - 'Your session has expired. Redirecting to login...', - 'warning' - ); - setTimeout(() => { - window.location.href = - '/login?next=' + encodeURIComponent(window.location.href); - }, 1500); - throw new Error('Session expired'); - } + // Also check if we were redirected to login page (for cases where 401 isn't returned) + if (response.redirected && response.url.includes('/login')) { + showToast( + 'Session Expired', + 'Your session has expired. Redirecting to login...', + 'warning', + ); + setTimeout(() => { + window.location.href = + '/login?next=' + encodeURIComponent(window.location.href); + }, 1500); + throw new Error('Session expired'); + } - return response; + return response; } function editTag(areaId, tagName, tagValue) { - const newValue = prompt(`Edit ${tagName}:`, tagValue); - if (newValue !== null && newValue !== tagValue) { - setAreaTag(areaId, tagName, newValue); - } + const newValue = prompt(`Edit ${tagName}:`, tagValue); + if (newValue !== null && newValue !== tagValue) { + setAreaTag(areaId, tagName, newValue); + } } function addTag(areaId) { - const tagName = prompt('Enter tag name:'); - if (tagName) { - const tagValue = prompt('Enter tag value:'); - if (tagValue) { - setAreaTag(areaId, tagName, tagValue); - } - } + const tagName = prompt('Enter tag name:'); + if (tagName) { + const tagValue = prompt('Enter tag value:'); + if (tagValue) { + setAreaTag(areaId, tagName, tagValue); + } + } } function setAreaTag(areaId, tagName, tagValue) { - apiFetch('/api/set_area_tag', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ id: areaId, name: tagName, value: tagValue }), - }) - .then((response) => response.json()) - .then((data) => { - if (data.error) { - showToast( - 'Error', - data.error.message || 'Failed to update tag', - 'error' - ); - } else { - location.reload(); - } - }) - .catch((error) => { - if (error.message !== 'Session expired') { - showToast('Error', 'Failed to update tag', 'error'); - } - }); + apiFetch('/api/set_area_tag', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id: areaId, name: tagName, value: tagValue }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.error) { + showToast( + 'Error', + data.error.message || 'Failed to update tag', + 'error', + ); + } else { + location.reload(); + } + }) + .catch((error) => { + if (error.message !== 'Session expired') { + showToast('Error', 'Failed to update tag', 'error'); + } + }); } function removeTag(areaId, tagName) { - apiFetch('/api/remove_area_tag', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ id: areaId, tag: tagName }), - }) - .then((response) => response.json()) - .then((data) => { - if (data.error) { - showToast( - 'Error', - data.error.message || 'Failed to remove tag', - 'error' - ); - } else { - showToast('Success', 'Tag removed successfully', 'success'); - setTimeout(() => location.reload(), 1000); - } - }) - .catch((error) => { - if (error.message !== 'Session expired') { - showToast('Error', 'Failed to remove tag', 'error'); - } - }); + apiFetch('/api/remove_area_tag', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id: areaId, tag: tagName }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.error) { + showToast( + 'Error', + data.error.message || 'Failed to remove tag', + 'error', + ); + } else { + showToast('Success', 'Tag removed successfully', 'success'); + setTimeout(() => location.reload(), 1000); + } + }) + .catch((error) => { + if (error.message !== 'Session expired') { + showToast('Error', 'Failed to remove tag', 'error'); + } + }); } function removeArea(areaId) { - apiFetch('/api/remove_area', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ id: areaId }), - }) - .then((response) => response.json()) - .then((data) => { - if (data.error) { - showToast( - 'Error', - data.error.message || 'Failed to remove area', - 'error' - ); - } else { - showToast('Success', 'Area removed successfully', 'success'); - setTimeout(() => (window.location.href = '/select_area'), 1500); - } - }) - .catch((error) => { - if (error.message !== 'Session expired') { - showToast('Error', 'Failed to remove area', 'error'); - } - }); + apiFetch('/api/remove_area', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id: areaId }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.error) { + showToast( + 'Error', + data.error.message || 'Failed to remove area', + 'error', + ); + } else { + showToast('Success', 'Area removed successfully', 'success'); + setTimeout(() => (window.location.href = '/select_area'), 1500); + } + }) + .catch((error) => { + if (error.message !== 'Session expired') { + showToast('Error', 'Failed to remove area', 'error'); + } + }); } diff --git a/static/js/validation.js b/static/js/validation.js index 94cf602..1364436 100644 --- a/static/js/validation.js +++ b/static/js/validation.js @@ -1,79 +1,79 @@ // Common validation functions function validateKey(key, existingKeys) { - key = key ? key.trim() : ''; - if (!key) { - return { isValid: false, message: 'Key cannot be empty' }; - } - if (existingKeys && existingKeys.includes(key)) { - return { isValid: false, message: 'Key already exists' }; - } - if (!/^[a-zA-Z][a-zA-Z0-9_:]*$/.test(key)) { - return { - isValid: false, - message: - 'Key must start with a letter and contain only letters, numbers, underscores, and colons', - }; - } - return { isValid: true }; + key = key ? key.trim() : ''; + if (!key) { + return { isValid: false, message: 'Key cannot be empty' }; + } + if (existingKeys && existingKeys.includes(key)) { + return { isValid: false, message: 'Key already exists' }; + } + if (!/^[a-zA-Z][a-zA-Z0-9_:]*$/.test(key)) { + return { + isValid: false, + message: + 'Key must start with a letter and contain only letters, numbers, underscores, and colons', + }; + } + return { isValid: true }; } function validateNumericValue(value, type) { - if (!value || value.trim() === '') { - return { isValid: false, message: 'Value cannot be empty' }; - } + if (!value || value.trim() === '') { + return { isValid: false, message: 'Value cannot be empty' }; + } - value = value.toString().trim(); + value = value.toString().trim(); - if (type === 'integer') { - if (!/^\d+$/.test(value)) { - return { - isValid: false, - message: 'Value must be a valid integer (no decimal points)', - }; - } - const num = parseInt(value, 10); - if (num < 0) { - return { isValid: false, message: 'Value must be non-negative' }; - } - return { isValid: true, value: num }; - } else if (type === 'number') { - if (!/^\d*\.?\d*$/.test(value)) { - return { - isValid: false, - message: - 'Value must contain only digits and at most one decimal point', - }; - } - const num = parseFloat(value); - if (isNaN(num)) { - return { isValid: false, message: 'Value must be a valid number' }; - } - if (num < 0) { - return { isValid: false, message: 'Value must be non-negative' }; - } - return { isValid: true, value: num }; - } + if (type === 'integer') { + if (!/^\d+$/.test(value)) { + return { + isValid: false, + message: 'Value must be a valid integer (no decimal points)', + }; + } + const num = parseInt(value, 10); + if (num < 0) { + return { isValid: false, message: 'Value must be non-negative' }; + } + return { isValid: true, value: num }; + } else if (type === 'number') { + if (!/^\d*\.?\d*$/.test(value)) { + return { + isValid: false, + message: + 'Value must contain only digits and at most one decimal point', + }; + } + const num = parseFloat(value); + if (isNaN(num)) { + return { isValid: false, message: 'Value must be a valid number' }; + } + if (num < 0) { + return { isValid: false, message: 'Value must be non-negative' }; + } + return { isValid: true, value: num }; + } - return { isValid: true, value: value.trim() }; + return { isValid: true, value: value.trim() }; } function validateValue(value, requirements) { - if (!value || value.trim() === '') { - return { isValid: false, message: 'Value cannot be empty' }; - } + if (!value || value.trim() === '') { + return { isValid: false, message: 'Value cannot be empty' }; + } - if (requirements && requirements.type) { - if (requirements.type === 'integer' || requirements.type === 'number') { - return validateNumericValue(value, requirements.type); - } else if (requirements.allowed_values) { - if (!requirements.allowed_values.includes(value)) { - return { - isValid: false, - message: `Value must be one of: ${requirements.allowed_values.join(', ')}`, - }; - } - } - } + if (requirements && requirements.type) { + if (requirements.type === 'integer' || requirements.type === 'number') { + return validateNumericValue(value, requirements.type); + } else if (requirements.allowed_values) { + if (!requirements.allowed_values.includes(value)) { + return { + isValid: false, + message: `Value must be one of: ${requirements.allowed_values.join(', ')}`, + }; + } + } + } - return { isValid: true, value: value.trim() }; + return { isValid: true, value: value.trim() }; } From 4ce38b6cb2770a0e7adad3ea5547c3484ba51e5c Mon Sep 17 00:00:00 2001 From: escapedcat Date: Sat, 21 Feb 2026 12:37:39 +0100 Subject: [PATCH 3/5] style: enable modern JS lint rules and fix code --- biome.json | 13 ++++-------- static/js/map-editor.js | 28 +++++++++++++----------- static/js/osm-geojson-generator.js | 34 +++++++++++++----------------- static/js/script.js | 11 +++------- static/js/validation.js | 10 ++++----- 5 files changed, 43 insertions(+), 53 deletions(-) diff --git a/biome.json b/biome.json index 572a471..abb26b6 100644 --- a/biome.json +++ b/biome.json @@ -7,20 +7,15 @@ "rules": { "recommended": true, "complexity": { - "noForEach": "off", - "useOptionalChain": "off", - "useArrowFunction": "off" + "noForEach": "off" }, "style": { - "noParameterAssign": "off", "noUselessElse": "off", - "useNumberNamespace": "off", - "useTemplate": "off", - "useExponentiationOperator": "off" + "useExponentiationOperator": "off", + "noParameterAssign": "off" }, "suspicious": { - "noAssignInExpressions": "off", - "noGlobalIsNan": "off" + "noAssignInExpressions": "off" } }, "ignore": ["**/.venv/**", "**/node_modules/**", "**/__pycache__/**"] diff --git a/static/js/map-editor.js b/static/js/map-editor.js index a631328..950e847 100644 --- a/static/js/map-editor.js +++ b/static/js/map-editor.js @@ -232,7 +232,7 @@ function initMapEditor(options = {}) { } // Handle draw created - map.on(L.Draw.Event.CREATED, function (e) { + map.on(L.Draw.Event.CREATED, (e) => { const layer = e.layer; // Clear existing and add new drawnItems.clearLayers(); @@ -241,12 +241,12 @@ function initMapEditor(options = {}) { }); // Handle draw edited - map.on(L.Draw.Event.EDITED, function (e) { + map.on(L.Draw.Event.EDITED, (e) => { updateGeoJsonFromDrawnItems(); }); // Handle draw deleted - map.on(L.Draw.Event.DELETED, function (e) { + map.on(L.Draw.Event.DELETED, (e) => { currentGeoJson = null; if (geoJsonLayer) { map.removeLayer(geoJsonLayer); @@ -294,7 +294,7 @@ function initMapEditor(options = {}) { if (typeof showToast === 'function') { showToast( 'Error', - 'Invalid GeoJSON: ' + error.message, + `Invalid GeoJSON: ${error.message}`, 'error', ); } @@ -316,7 +316,7 @@ function initMapEditor(options = {}) { if (typeof showToast === 'function') { showToast( 'Error', - 'Invalid GeoJSON: ' + error.message, + `Invalid GeoJSON: ${error.message}`, 'error', ); } @@ -356,7 +356,7 @@ function initMapEditor(options = {}) { // 0 = pure convex (concavity = Infinity) // 10 = tighter fit (concavity = 1) function sliderToConcavity(sliderVal) { - if (sliderVal === 0) return Infinity; + if (sliderVal === 0) return Number.POSITIVE_INFINITY; return Math.max(1, 21 - sliderVal * 2); } @@ -376,7 +376,7 @@ function initMapEditor(options = {}) { } } const geometry = geojson.geometry || geojson; - if (geometry && geometry.coordinates) { + if (geometry?.coordinates) { countCoords(geometry.coordinates); } return count; @@ -501,9 +501,11 @@ function initMapEditor(options = {}) { } const tolerance = sliderToTolerance( - parseFloat(elements.shapeSimplifySlider?.value || 0), + Number.parseFloat(elements.shapeSimplifySlider?.value || 0), + ); + const buffer = Number.parseFloat( + elements.shapeBufferSlider?.value || 0, ); - const buffer = parseFloat(elements.shapeBufferSlider?.value || 0); let processed = feature; @@ -528,7 +530,9 @@ function initMapEditor(options = {}) { typeof turf !== 'undefined' ) { const concavity = sliderToConcavity( - parseFloat(elements.shapeTightnessSlider?.value || 0), + Number.parseFloat( + elements.shapeTightnessSlider?.value || 0, + ), ); try { @@ -658,7 +662,7 @@ function initMapEditor(options = {}) { } if (elements.shapeSimplifySlider) { elements.shapeSimplifySlider.addEventListener('input', function () { - const tolerance = sliderToTolerance(parseFloat(this.value)); + const tolerance = sliderToTolerance(Number.parseFloat(this.value)); elements.shapeSimplifyValue.textContent = formatTolerance(tolerance); processAndPreviewShape(); @@ -683,7 +687,7 @@ function initMapEditor(options = {}) { } if (elements.shapeTightnessSlider) { elements.shapeTightnessSlider.addEventListener('input', function () { - const val = parseInt(this.value); + const val = Number.parseInt(this.value); if (elements.shapeTightnessValue) { elements.shapeTightnessValue.textContent = formatTightness(val); } diff --git a/static/js/osm-geojson-generator.js b/static/js/osm-geojson-generator.js index 0745204..594c6e4 100644 --- a/static/js/osm-geojson-generator.js +++ b/static/js/osm-geojson-generator.js @@ -90,7 +90,7 @@ function initOsmGeojsonGenerator(options = {}) { } const geometry = geojson.geometry || geojson; - if (geometry && geometry.coordinates) { + if (geometry?.coordinates) { countCoords(geometry.coordinates); } @@ -121,7 +121,7 @@ function initOsmGeojsonGenerator(options = {}) { // 0 = pure convex (concavity = Infinity) // 10 = tighter fit (concavity = 1) function sliderToConcavity(sliderVal) { - if (sliderVal === 0) return Infinity; // Pure convex hull + if (sliderVal === 0) return Number.POSITIVE_INFINITY; // Pure convex hull // Map 1-10 to concavity values (higher = looser, lower = tighter) // 1 = 20 (loose), 10 = 1 (tight) return Math.max(1, 21 - sliderVal * 2); @@ -190,7 +190,7 @@ function initOsmGeojsonGenerator(options = {}) { const index = elements.resultsSelect.value; if (index === '') return; - const result = osmSearchResults[parseInt(index)]; + const result = osmSearchResults[Number.parseInt(index)]; if (!result || !result.geojson) { showError('Selected place has no boundary data'); return; @@ -210,13 +210,12 @@ function initOsmGeojsonGenerator(options = {}) { originalOsmGeojson = result.geojson; // Handle population data from OSM extratags - if ( - result.extratags && - result.extratags.population && - onPopulationFound - ) { - const populationValue = parseInt(result.extratags.population, 10); - if (!isNaN(populationValue)) { + if (result.extratags?.population && onPopulationFound) { + const populationValue = Number.parseInt( + result.extratags.population, + 10, + ); + if (!Number.isNaN(populationValue)) { const today = new Date().toISOString().split('T')[0]; onPopulationFound(populationValue, today); } @@ -320,9 +319,9 @@ function initOsmGeojsonGenerator(options = {}) { } const tolerance = sliderToTolerance( - parseFloat(elements.simplifySlider.value), + Number.parseFloat(elements.simplifySlider.value), ); - const buffer = parseFloat(elements.bufferSlider.value); + const buffer = Number.parseFloat(elements.bufferSlider.value); let processed = feature; @@ -342,12 +341,9 @@ function initOsmGeojsonGenerator(options = {}) { } // Apply mega simplify if checked - if ( - elements.megaSimplifyCheckbox && - elements.megaSimplifyCheckbox.checked - ) { + if (elements.megaSimplifyCheckbox?.checked) { const concavity = sliderToConcavity( - parseFloat(elements.tightnessSlider.value), + Number.parseFloat(elements.tightnessSlider.value), ); try { @@ -479,7 +475,7 @@ function initOsmGeojsonGenerator(options = {}) { if (elements.simplifySlider) { elements.simplifySlider.addEventListener('input', function () { - const tolerance = sliderToTolerance(parseFloat(this.value)); + const tolerance = sliderToTolerance(Number.parseFloat(this.value)); elements.simplifyValue.textContent = formatTolerance(tolerance); processAndPreviewGeojson(); }); @@ -513,7 +509,7 @@ function initOsmGeojsonGenerator(options = {}) { // Tightness slider if (elements.tightnessSlider) { elements.tightnessSlider.addEventListener('input', function () { - const val = parseInt(this.value); + const val = Number.parseInt(this.value); if (elements.tightnessValue) { elements.tightnessValue.textContent = formatTightness(val); } diff --git a/static/js/script.js b/static/js/script.js index 0c2e078..82ecc51 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -21,9 +21,7 @@ async function apiFetch(url, options = {}) { 'warning', ); setTimeout(() => { - window.location.href = - '/login?next=' + - encodeURIComponent(window.location.href); + window.location.href = `/login?next=${encodeURIComponent(window.location.href)}`; }, 1500); throw new Error('Session expired'); } @@ -41,9 +39,7 @@ async function apiFetch(url, options = {}) { 'warning', ); setTimeout(() => { - window.location.href = - '/login?next=' + - encodeURIComponent(window.location.href); + window.location.href = `/login?next=${encodeURIComponent(window.location.href)}`; }, 1500); throw new Error('Session expired'); } @@ -58,8 +54,7 @@ async function apiFetch(url, options = {}) { 'warning', ); setTimeout(() => { - window.location.href = - '/login?next=' + encodeURIComponent(window.location.href); + window.location.href = `/login?next=${encodeURIComponent(window.location.href)}`; }, 1500); throw new Error('Session expired'); } diff --git a/static/js/validation.js b/static/js/validation.js index 1364436..cbe9346 100644 --- a/static/js/validation.js +++ b/static/js/validation.js @@ -4,7 +4,7 @@ function validateKey(key, existingKeys) { if (!key) { return { isValid: false, message: 'Key cannot be empty' }; } - if (existingKeys && existingKeys.includes(key)) { + if (existingKeys?.includes(key)) { return { isValid: false, message: 'Key already exists' }; } if (!/^[a-zA-Z][a-zA-Z0-9_:]*$/.test(key)) { @@ -31,7 +31,7 @@ function validateNumericValue(value, type) { message: 'Value must be a valid integer (no decimal points)', }; } - const num = parseInt(value, 10); + const num = Number.parseInt(value, 10); if (num < 0) { return { isValid: false, message: 'Value must be non-negative' }; } @@ -44,8 +44,8 @@ function validateNumericValue(value, type) { 'Value must contain only digits and at most one decimal point', }; } - const num = parseFloat(value); - if (isNaN(num)) { + const num = Number.parseFloat(value); + if (Number.isNaN(num)) { return { isValid: false, message: 'Value must be a valid number' }; } if (num < 0) { @@ -62,7 +62,7 @@ function validateValue(value, requirements) { return { isValid: false, message: 'Value cannot be empty' }; } - if (requirements && requirements.type) { + if (requirements?.type) { if (requirements.type === 'integer' || requirements.type === 'number') { return validateNumericValue(value, requirements.type); } else if (requirements.allowed_values) { From 087896a926d329f8afb1ec43bb846a2614daa8ed Mon Sep 17 00:00:00 2001 From: escapedcat Date: Sat, 21 Feb 2026 12:49:50 +0100 Subject: [PATCH 4/5] style: use Biome defaults (2-space indent, double quotes) --- biome.json | 6 - static/js/map-editor.js | 256 ++++++++++++----------------- static/js/osm-geojson-generator.js | 179 +++++++++----------- static/js/script.js | 88 +++++----- static/js/validation.js | 35 ++-- 5 files changed, 251 insertions(+), 313 deletions(-) diff --git a/biome.json b/biome.json index abb26b6..ede00b5 100644 --- a/biome.json +++ b/biome.json @@ -21,12 +21,6 @@ "ignore": ["**/.venv/**", "**/node_modules/**", "**/__pycache__/**"] }, "formatter": { - "indentWidth": 4, "ignore": ["**/.venv/**", "**/node_modules/**", "**/__pycache__/**"] - }, - "javascript": { - "formatter": { - "quoteStyle": "single" - } } } diff --git a/static/js/map-editor.js b/static/js/map-editor.js index 950e847..4b5c84a 100644 --- a/static/js/map-editor.js +++ b/static/js/map-editor.js @@ -26,7 +26,7 @@ function initMapEditor(options = {}) { const { - containerId = 'map', + containerId = "map", initialGeoJson = null, onGeoJsonChange = null, } = options; @@ -45,39 +45,37 @@ function initMapEditor(options = {}) { const elements = { mapContainer: document.getElementById(containerId), // Raw GeoJSON editor - editGeoJsonBtn: document.getElementById('edit-geojson-btn'), - geoJsonEditor: document.getElementById('geojson-editor'), - geoJsonInput: document.getElementById('geojson-input'), - showBtn: document.getElementById('show-geojson-btn'), - saveLocallyBtn: document.getElementById('save-locally-btn'), - cancelBtn: document.getElementById('cancel-geojson-btn'), + editGeoJsonBtn: document.getElementById("edit-geojson-btn"), + geoJsonEditor: document.getElementById("geojson-editor"), + geoJsonInput: document.getElementById("geojson-input"), + showBtn: document.getElementById("show-geojson-btn"), + saveLocallyBtn: document.getElementById("save-locally-btn"), + cancelBtn: document.getElementById("cancel-geojson-btn"), // Shape editor - editShapeBtn: document.getElementById('edit-shape-btn'), - shapeEditor: document.getElementById('shape-editor'), - shapeSimplifySlider: document.getElementById('shape-simplify-slider'), - shapeSimplifyValue: document.getElementById('shape-simplify-value'), - shapeBufferSlider: document.getElementById('shape-buffer-slider'), - shapeBufferValue: document.getElementById('shape-buffer-value'), - shapeMegaSimplifyCheckbox: document.getElementById( - 'shape-mega-simplify', - ), - shapeTightnessSlider: document.getElementById('shape-tightness-slider'), - shapeTightnessValue: document.getElementById('shape-tightness-value'), - shapePointsCount: document.getElementById('shape-points-count'), - shapeShowOriginal: document.getElementById('shape-show-original'), - applyShapeBtn: document.getElementById('apply-shape-btn'), - cancelShapeBtn: document.getElementById('cancel-shape-btn'), + editShapeBtn: document.getElementById("edit-shape-btn"), + shapeEditor: document.getElementById("shape-editor"), + shapeSimplifySlider: document.getElementById("shape-simplify-slider"), + shapeSimplifyValue: document.getElementById("shape-simplify-value"), + shapeBufferSlider: document.getElementById("shape-buffer-slider"), + shapeBufferValue: document.getElementById("shape-buffer-value"), + shapeMegaSimplifyCheckbox: document.getElementById("shape-mega-simplify"), + shapeTightnessSlider: document.getElementById("shape-tightness-slider"), + shapeTightnessValue: document.getElementById("shape-tightness-value"), + shapePointsCount: document.getElementById("shape-points-count"), + shapeShowOriginal: document.getElementById("shape-show-original"), + applyShapeBtn: document.getElementById("apply-shape-btn"), + cancelShapeBtn: document.getElementById("cancel-shape-btn"), }; // Initialize map if (!elements.mapContainer) { - console.error('[MapEditor] Map container not found:', containerId); + console.error("[MapEditor] Map container not found:", containerId); return null; } const map = L.map(containerId).setView([0, 0], 2); - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors', + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { + attribution: "© OpenStreetMap contributors", }).addTo(map); // Initialize draw control @@ -90,13 +88,13 @@ function initMapEditor(options = {}) { allowIntersection: false, showArea: true, shapeOptions: { - color: '#3388ff', + color: "#3388ff", weight: 3, }, }, rectangle: { shapeOptions: { - color: '#3388ff', + color: "#3388ff", weight: 3, }, }, @@ -118,7 +116,7 @@ function initMapEditor(options = {}) { if (drawnItems.getLayers().length === 0) { currentGeoJson = null; if (elements.geoJsonInput) { - elements.geoJsonInput.value = ''; + elements.geoJsonInput.value = ""; } if (onGeoJsonChange) { onGeoJsonChange(null); @@ -132,11 +130,7 @@ function initMapEditor(options = {}) { // Keep textarea in sync if (elements.geoJsonInput) { - elements.geoJsonInput.value = JSON.stringify( - currentGeoJson, - null, - 2, - ); + elements.geoJsonInput.value = JSON.stringify(currentGeoJson, null, 2); } map.fitBounds(drawnItems.getBounds()); @@ -160,7 +154,7 @@ function initMapEditor(options = {}) { if (!geoJson) { currentGeoJson = null; if (elements.geoJsonInput) { - elements.geoJsonInput.value = ''; + elements.geoJsonInput.value = ""; } if (onGeoJsonChange) { onGeoJsonChange(null); @@ -169,16 +163,16 @@ function initMapEditor(options = {}) { } // Handle string input - if (typeof geoJson === 'string') { + if (typeof geoJson === "string") { geoJson = JSON.parse(geoJson); } // Extract geometry if wrapped in Feature let geometry = geoJson; - if (geoJson.type === 'Feature') { + if (geoJson.type === "Feature") { geometry = geoJson.geometry; } else if ( - geoJson.type === 'FeatureCollection' && + geoJson.type === "FeatureCollection" && geoJson.features && geoJson.features.length > 0 ) { @@ -190,7 +184,7 @@ function initMapEditor(options = {}) { // Wrap back as Feature for display in Leaflet const featureCollection = { - type: 'Feature', + type: "Feature", geometry: geometry, }; @@ -206,11 +200,7 @@ function initMapEditor(options = {}) { // Update textarea with current GeoJSON (keep them in sync) if (elements.geoJsonInput) { - elements.geoJsonInput.value = JSON.stringify( - currentGeoJson, - null, - 2, - ); + elements.geoJsonInput.value = JSON.stringify(currentGeoJson, null, 2); } if (onGeoJsonChange) { @@ -219,13 +209,9 @@ function initMapEditor(options = {}) { return true; } catch (error) { - console.error('[MapEditor] Error adding GeoJSON to map:', error); - if (typeof showToast === 'function') { - showToast( - 'Error', - `Invalid GeoJSON: ${error.message}`, - 'error', - ); + console.error("[MapEditor] Error adding GeoJSON to map:", error); + if (typeof showToast === "function") { + showToast("Error", `Invalid GeoJSON: ${error.message}`, "error"); } return false; } @@ -253,7 +239,7 @@ function initMapEditor(options = {}) { geoJsonLayer = null; } if (elements.geoJsonInput) { - elements.geoJsonInput.value = ''; + elements.geoJsonInput.value = ""; } if (onGeoJsonChange) { onGeoJsonChange(null); @@ -263,21 +249,17 @@ function initMapEditor(options = {}) { // Raw GeoJSON editor functionality function showEditor() { if (elements.geoJsonEditor) { - elements.geoJsonEditor.style.display = 'block'; + elements.geoJsonEditor.style.display = "block"; } if (currentGeoJson && elements.geoJsonInput) { - elements.geoJsonInput.value = JSON.stringify( - currentGeoJson, - null, - 2, - ); + elements.geoJsonInput.value = JSON.stringify(currentGeoJson, null, 2); previouslySavedGeoJson = JSON.parse(JSON.stringify(currentGeoJson)); } } function hideEditor() { if (elements.geoJsonEditor) { - elements.geoJsonEditor.style.display = 'none'; + elements.geoJsonEditor.style.display = "none"; } } @@ -285,18 +267,14 @@ function initMapEditor(options = {}) { try { const geoJson = JSON.parse(elements.geoJsonInput.value); if (updateGeoJson(geoJson)) { - if (typeof showToast === 'function') { - showToast('Success', 'GeoJSON preview updated', 'success'); + if (typeof showToast === "function") { + showToast("Success", "GeoJSON preview updated", "success"); } } } catch (error) { - console.error('[MapEditor] Error parsing GeoJSON:', error); - if (typeof showToast === 'function') { - showToast( - 'Error', - `Invalid GeoJSON: ${error.message}`, - 'error', - ); + console.error("[MapEditor] Error parsing GeoJSON:", error); + if (typeof showToast === "function") { + showToast("Error", `Invalid GeoJSON: ${error.message}`, "error"); } } } @@ -307,18 +285,14 @@ function initMapEditor(options = {}) { if (updateGeoJson(geoJson)) { hideEditor(); previouslySavedGeoJson = currentGeoJson; - if (typeof showToast === 'function') { - showToast('Success', 'GeoJSON saved locally', 'success'); + if (typeof showToast === "function") { + showToast("Success", "GeoJSON saved locally", "success"); } } } catch (error) { - console.error('[MapEditor] Error saving GeoJSON:', error); - if (typeof showToast === 'function') { - showToast( - 'Error', - `Invalid GeoJSON: ${error.message}`, - 'error', - ); + console.error("[MapEditor] Error saving GeoJSON:", error); + if (typeof showToast === "function") { + showToast("Error", `Invalid GeoJSON: ${error.message}`, "error"); } } } @@ -344,7 +318,7 @@ function initMapEditor(options = {}) { } function formatTolerance(val) { - if (val === 0) return '0'; + if (val === 0) return "0"; if (val < 0.00001) return val.toExponential(1); if (val < 0.0001) return val.toFixed(6); if (val < 0.001) return val.toFixed(5); @@ -361,15 +335,15 @@ function initMapEditor(options = {}) { } function formatTightness(val) { - if (val === 0) return 'Loose'; - if (val === 10) return 'Tight'; + if (val === 0) return "Loose"; + if (val === 10) return "Tight"; return val.toString(); } function countGeojsonPoints(geojson) { let count = 0; function countCoords(coords) { - if (typeof coords[0] === 'number') { + if (typeof coords[0] === "number") { count++; } else { coords.forEach((c) => countCoords(c)); @@ -384,11 +358,11 @@ function initMapEditor(options = {}) { function showShapeEditor() { if (!currentGeoJson) { - if (typeof showToast === 'function') { + if (typeof showToast === "function") { showToast( - 'Warning', - 'No shape to edit. Draw or import a polygon first.', - 'warning', + "Warning", + "No shape to edit. Draw or import a polygon first.", + "warning", ); } return; @@ -403,11 +377,11 @@ function initMapEditor(options = {}) { // Reset sliders to neutral (no changes) if (elements.shapeSimplifySlider) { elements.shapeSimplifySlider.value = 0; - elements.shapeSimplifyValue.textContent = '0'; + elements.shapeSimplifyValue.textContent = "0"; } if (elements.shapeBufferSlider) { elements.shapeBufferSlider.value = 0; - elements.shapeBufferValue.textContent = '0'; + elements.shapeBufferValue.textContent = "0"; } if (elements.shapeMegaSimplifyCheckbox) { elements.shapeMegaSimplifyCheckbox.checked = false; @@ -415,12 +389,12 @@ function initMapEditor(options = {}) { if (elements.shapeTightnessSlider) { elements.shapeTightnessSlider.value = 0; elements.shapeTightnessSlider.disabled = true; - elements.shapeTightnessValue.textContent = 'Loose'; + elements.shapeTightnessValue.textContent = "Loose"; } // Show editor if (elements.shapeEditor) { - elements.shapeEditor.style.display = 'block'; + elements.shapeEditor.style.display = "block"; } // Show original shape and initial preview @@ -430,7 +404,7 @@ function initMapEditor(options = {}) { function hideShapeEditor() { if (elements.shapeEditor) { - elements.shapeEditor.style.display = 'none'; + elements.shapeEditor.style.display = "none"; } // Remove preview layers if (shapePreviewLayer) { @@ -461,11 +435,11 @@ function initMapEditor(options = {}) { // Wrap as Feature if needed let feature = originalShapeGeoJson; if ( - originalShapeGeoJson.type !== 'Feature' && - originalShapeGeoJson.type !== 'FeatureCollection' + originalShapeGeoJson.type !== "Feature" && + originalShapeGeoJson.type !== "FeatureCollection" ) { feature = { - type: 'Feature', + type: "Feature", geometry: originalShapeGeoJson, properties: {}, }; @@ -474,9 +448,9 @@ function initMapEditor(options = {}) { // Create dashed line style for reference originalShapeLayer = L.geoJSON(feature, { style: { - color: '#ff6600', + color: "#ff6600", weight: 2, - dashArray: '5, 5', + dashArray: "5, 5", fillOpacity: 0, interactive: false, }, @@ -490,11 +464,11 @@ function initMapEditor(options = {}) { // Wrap as Feature if needed let feature = originalShapeGeoJson; if ( - originalShapeGeoJson.type !== 'Feature' && - originalShapeGeoJson.type !== 'FeatureCollection' + originalShapeGeoJson.type !== "Feature" && + originalShapeGeoJson.type !== "FeatureCollection" ) { feature = { - type: 'Feature', + type: "Feature", geometry: originalShapeGeoJson, properties: {}, }; @@ -503,21 +477,19 @@ function initMapEditor(options = {}) { const tolerance = sliderToTolerance( Number.parseFloat(elements.shapeSimplifySlider?.value || 0), ); - const buffer = Number.parseFloat( - elements.shapeBufferSlider?.value || 0, - ); + const buffer = Number.parseFloat(elements.shapeBufferSlider?.value || 0); let processed = feature; // Apply buffer if > 0 - if (buffer > 0 && typeof turf !== 'undefined') { + if (buffer > 0 && typeof turf !== "undefined") { processed = turf.buffer(processed, buffer, { - units: 'kilometers', + units: "kilometers", }); } // Apply simplification if > 0 - if (tolerance > 0 && typeof turf !== 'undefined') { + if (tolerance > 0 && typeof turf !== "undefined") { processed = turf.simplify(processed, { tolerance: tolerance, highQuality: true, @@ -527,12 +499,10 @@ function initMapEditor(options = {}) { // Apply mega simplify if checked if ( elements.shapeMegaSimplifyCheckbox?.checked && - typeof turf !== 'undefined' + typeof turf !== "undefined" ) { const concavity = sliderToConcavity( - Number.parseFloat( - elements.shapeTightnessSlider?.value || 0, - ), + Number.parseFloat(elements.shapeTightnessSlider?.value || 0), ); try { @@ -542,11 +512,11 @@ function initMapEditor(options = {}) { }); if (!processed) { - console.warn('[MapEditor] Convex hull returned null'); + console.warn("[MapEditor] Convex hull returned null"); processed = turf.convex(feature); } } catch (e) { - console.warn('[MapEditor] Convex hull failed:', e.message); + console.warn("[MapEditor] Convex hull failed:", e.message); // Keep original processed value } } @@ -564,9 +534,9 @@ function initMapEditor(options = {}) { // Add preview layer shapePreviewLayer = L.geoJSON(processed, { style: { - color: '#3388ff', + color: "#3388ff", weight: 3, - fillColor: '#3388ff', + fillColor: "#3388ff", fillOpacity: 0.2, }, }).addTo(map); @@ -580,21 +550,17 @@ function initMapEditor(options = {}) { elements.shapePointsCount.textContent = `Points: ${count}`; } } catch (error) { - console.error('[MapEditor] Error processing shape:', error); - if (typeof showToast === 'function') { - showToast( - 'Error', - `Error processing: ${error.message}`, - 'error', - ); + console.error("[MapEditor] Error processing shape:", error); + if (typeof showToast === "function") { + showToast("Error", `Error processing: ${error.message}`, "error"); } } } function applyShapeChanges() { if (!shapePreviewLayer) { - if (typeof showToast === 'function') { - showToast('Warning', 'No changes to apply', 'warning'); + if (typeof showToast === "function") { + showToast("Warning", "No changes to apply", "warning"); } return; } @@ -603,11 +569,11 @@ function initMapEditor(options = {}) { const previewGeoJson = shapePreviewLayer.toGeoJSON(); let geometry = previewGeoJson; if ( - previewGeoJson.type === 'FeatureCollection' && + previewGeoJson.type === "FeatureCollection" && previewGeoJson.features.length > 0 ) { geometry = previewGeoJson.features[0].geometry; - } else if (previewGeoJson.type === 'Feature') { + } else if (previewGeoJson.type === "Feature") { geometry = previewGeoJson.geometry; } @@ -622,8 +588,8 @@ function initMapEditor(options = {}) { // Apply the processed geometry updateGeoJson(geometry); - if (typeof showToast === 'function') { - showToast('Success', 'Shape changes applied', 'success'); + if (typeof showToast === "function") { + showToast("Success", "Shape changes applied", "success"); } } @@ -644,49 +610,45 @@ function initMapEditor(options = {}) { // Set up event listeners - Raw GeoJSON editor if (elements.editGeoJsonBtn) { - elements.editGeoJsonBtn.addEventListener('click', showEditor); + elements.editGeoJsonBtn.addEventListener("click", showEditor); } if (elements.showBtn) { - elements.showBtn.addEventListener('click', showGeoJsonPreview); + elements.showBtn.addEventListener("click", showGeoJsonPreview); } if (elements.saveLocallyBtn) { - elements.saveLocallyBtn.addEventListener('click', saveLocally); + elements.saveLocallyBtn.addEventListener("click", saveLocally); } if (elements.cancelBtn) { - elements.cancelBtn.addEventListener('click', cancelEditing); + elements.cancelBtn.addEventListener("click", cancelEditing); } // Set up event listeners - Shape editor if (elements.editShapeBtn) { - elements.editShapeBtn.addEventListener('click', showShapeEditor); + elements.editShapeBtn.addEventListener("click", showShapeEditor); } if (elements.shapeSimplifySlider) { - elements.shapeSimplifySlider.addEventListener('input', function () { + elements.shapeSimplifySlider.addEventListener("input", function () { const tolerance = sliderToTolerance(Number.parseFloat(this.value)); - elements.shapeSimplifyValue.textContent = - formatTolerance(tolerance); + elements.shapeSimplifyValue.textContent = formatTolerance(tolerance); processAndPreviewShape(); }); } if (elements.shapeBufferSlider) { - elements.shapeBufferSlider.addEventListener('input', function () { + elements.shapeBufferSlider.addEventListener("input", function () { elements.shapeBufferValue.textContent = this.value; processAndPreviewShape(); }); } if (elements.shapeMegaSimplifyCheckbox) { - elements.shapeMegaSimplifyCheckbox.addEventListener( - 'change', - function () { - if (elements.shapeTightnessSlider) { - elements.shapeTightnessSlider.disabled = !this.checked; - } - processAndPreviewShape(); - }, - ); + elements.shapeMegaSimplifyCheckbox.addEventListener("change", function () { + if (elements.shapeTightnessSlider) { + elements.shapeTightnessSlider.disabled = !this.checked; + } + processAndPreviewShape(); + }); } if (elements.shapeTightnessSlider) { - elements.shapeTightnessSlider.addEventListener('input', function () { + elements.shapeTightnessSlider.addEventListener("input", function () { const val = Number.parseInt(this.value); if (elements.shapeTightnessValue) { elements.shapeTightnessValue.textContent = formatTightness(val); @@ -696,15 +658,15 @@ function initMapEditor(options = {}) { } if (elements.shapeShowOriginal) { elements.shapeShowOriginal.addEventListener( - 'change', + "change", updateOriginalShapeLayer, ); } if (elements.applyShapeBtn) { - elements.applyShapeBtn.addEventListener('click', applyShapeChanges); + elements.applyShapeBtn.addEventListener("click", applyShapeChanges); } if (elements.cancelShapeBtn) { - elements.cancelShapeBtn.addEventListener('click', cancelShapeEditing); + elements.cancelShapeBtn.addEventListener("click", cancelShapeEditing); } // Load initial GeoJSON if provided @@ -729,6 +691,6 @@ function initMapEditor(options = {}) { } // Export for module systems if available -if (typeof module !== 'undefined' && module.exports) { +if (typeof module !== "undefined" && module.exports) { module.exports = { initMapEditor }; } diff --git a/static/js/osm-geojson-generator.js b/static/js/osm-geojson-generator.js index 594c6e4..e0492a7 100644 --- a/static/js/osm-geojson-generator.js +++ b/static/js/osm-geojson-generator.js @@ -26,29 +26,29 @@ function initOsmGeojsonGenerator(options = {}) { } = options; if (!mapEditor) { - console.error('[OsmGeojsonGenerator] mapEditor is required'); + console.error("[OsmGeojsonGenerator] mapEditor is required"); return null; } // DOM Elements const elements = { - searchInput: document.getElementById('osm-search-input'), - searchBtn: document.getElementById('osm-search-btn'), - resultsContainer: document.getElementById('search-results-container'), - resultsSelect: document.getElementById('search-results'), - controls: document.getElementById('geojson-controls'), - simplifySlider: document.getElementById('simplify-slider'), - simplifyValue: document.getElementById('simplify-value'), - bufferSlider: document.getElementById('buffer-slider'), - bufferValue: document.getElementById('buffer-value'), - megaSimplifyCheckbox: document.getElementById('mega-simplify'), - tightnessSlider: document.getElementById('tightness-slider'), - tightnessValue: document.getElementById('tightness-value'), - pointsCount: document.getElementById('points-count'), - showOriginalBoundary: document.getElementById('show-original-boundary'), - applyBtn: document.getElementById('apply-geojson-btn'), - loading: document.getElementById('geojson-loading'), - error: document.getElementById('geojson-error'), + searchInput: document.getElementById("osm-search-input"), + searchBtn: document.getElementById("osm-search-btn"), + resultsContainer: document.getElementById("search-results-container"), + resultsSelect: document.getElementById("search-results"), + controls: document.getElementById("geojson-controls"), + simplifySlider: document.getElementById("simplify-slider"), + simplifyValue: document.getElementById("simplify-value"), + bufferSlider: document.getElementById("buffer-slider"), + bufferValue: document.getElementById("buffer-value"), + megaSimplifyCheckbox: document.getElementById("mega-simplify"), + tightnessSlider: document.getElementById("tightness-slider"), + tightnessValue: document.getElementById("tightness-value"), + pointsCount: document.getElementById("points-count"), + showOriginalBoundary: document.getElementById("show-original-boundary"), + applyBtn: document.getElementById("apply-geojson-btn"), + loading: document.getElementById("geojson-loading"), + error: document.getElementById("geojson-error"), }; // State @@ -62,19 +62,19 @@ function initOsmGeojsonGenerator(options = {}) { function showError(message) { if (elements.error) { elements.error.textContent = message; - elements.error.style.display = 'block'; + elements.error.style.display = "block"; } } function hideError() { if (elements.error) { - elements.error.style.display = 'none'; + elements.error.style.display = "none"; } } function showLoading(show) { if (elements.loading) { - elements.loading.style.display = show ? 'block' : 'none'; + elements.loading.style.display = show ? "block" : "none"; } } @@ -82,7 +82,7 @@ function initOsmGeojsonGenerator(options = {}) { let count = 0; function countCoords(coords) { - if (typeof coords[0] === 'number') { + if (typeof coords[0] === "number") { count++; } else { coords.forEach((c) => countCoords(c)); @@ -109,7 +109,7 @@ function initOsmGeojsonGenerator(options = {}) { } function formatTolerance(val) { - if (val === 0) return '0'; + if (val === 0) return "0"; if (val < 0.00001) return val.toExponential(1); if (val < 0.0001) return val.toFixed(6); if (val < 0.001) return val.toFixed(5); @@ -128,8 +128,8 @@ function initOsmGeojsonGenerator(options = {}) { } function formatTightness(val) { - if (val === 0) return 'Loose'; - if (val === 10) return 'Tight'; + if (val === 0) return "Loose"; + if (val === 10) return "Tight"; return val.toString(); } @@ -137,15 +137,15 @@ function initOsmGeojsonGenerator(options = {}) { async function searchOSM() { const query = elements.searchInput.value.trim(); if (!query) { - if (typeof showToast === 'function') { - showToast('Warning', 'Please enter a search term', 'warning'); + if (typeof showToast === "function") { + showToast("Warning", "Please enter a search term", "warning"); } return; } hideError(); - elements.resultsContainer.style.display = 'none'; - elements.controls.style.display = 'none'; + elements.resultsContainer.style.display = "none"; + elements.controls.style.display = "none"; showLoading(true); try { @@ -160,7 +160,7 @@ function initOsmGeojsonGenerator(options = {}) { if (results.length === 0) { showError( - 'No administrative areas found. Try a different search term.', + "No administrative areas found. Try a different search term.", ); return; } @@ -170,16 +170,16 @@ function initOsmGeojsonGenerator(options = {}) { elements.resultsSelect.innerHTML = ''; results.forEach((r, index) => { - const option = document.createElement('option'); + const option = document.createElement("option"); option.value = index; option.textContent = r.display_name; elements.resultsSelect.appendChild(option); }); - elements.resultsContainer.style.display = 'block'; + elements.resultsContainer.style.display = "block"; } catch (error) { - console.error('[OsmGeojsonGenerator] Search error:', error); - showError(error.message || 'Search failed'); + console.error("[OsmGeojsonGenerator] Search error:", error); + showError(error.message || "Search failed"); } finally { showLoading(false); } @@ -188,11 +188,11 @@ function initOsmGeojsonGenerator(options = {}) { // Handle search result selection async function onSearchResultSelect() { const index = elements.resultsSelect.value; - if (index === '') return; + if (index === "") return; const result = osmSearchResults[Number.parseInt(index)]; if (!result || !result.geojson) { - showError('Selected place has no boundary data'); + showError("Selected place has no boundary data"); return; } @@ -201,7 +201,7 @@ function initOsmGeojsonGenerator(options = {}) { if (currentGeoJson && mapEditor.drawnItems.getLayers().length > 0) { const confirmed = await confirmPolygonReplacement(); if (!confirmed) { - elements.resultsSelect.value = ''; + elements.resultsSelect.value = ""; return; } } @@ -211,23 +211,18 @@ function initOsmGeojsonGenerator(options = {}) { // Handle population data from OSM extratags if (result.extratags?.population && onPopulationFound) { - const populationValue = Number.parseInt( - result.extratags.population, - 10, - ); + const populationValue = Number.parseInt(result.extratags.population, 10); if (!Number.isNaN(populationValue)) { - const today = new Date().toISOString().split('T')[0]; + const today = new Date().toISOString().split("T")[0]; onPopulationFound(populationValue, today); } } // Reset sliders to defaults elements.simplifySlider.value = 5; // Maps to ~0.001 tolerance - elements.simplifyValue.textContent = formatTolerance( - sliderToTolerance(5), - ); + elements.simplifyValue.textContent = formatTolerance(sliderToTolerance(5)); elements.bufferSlider.value = 0.1; - elements.bufferValue.textContent = '0.1'; + elements.bufferValue.textContent = "0.1"; // Reset mega simplify controls if (elements.megaSimplifyCheckbox) { @@ -238,11 +233,11 @@ function initOsmGeojsonGenerator(options = {}) { elements.tightnessSlider.disabled = true; } if (elements.tightnessValue) { - elements.tightnessValue.textContent = 'Loose'; + elements.tightnessValue.textContent = "Loose"; } // Show controls and process - elements.controls.style.display = 'block'; + elements.controls.style.display = "block"; // Show original boundary and process updateOriginalBoundaryLayer(); @@ -253,7 +248,7 @@ function initOsmGeojsonGenerator(options = {}) { function confirmPolygonReplacement() { return new Promise((resolve) => { const result = confirm( - 'Replace existing polygon with the selected OSM boundary?', + "Replace existing polygon with the selected OSM boundary?", ); resolve(result); }); @@ -276,11 +271,11 @@ function initOsmGeojsonGenerator(options = {}) { // Wrap as Feature if needed let feature = originalOsmGeojson; if ( - originalOsmGeojson.type !== 'Feature' && - originalOsmGeojson.type !== 'FeatureCollection' + originalOsmGeojson.type !== "Feature" && + originalOsmGeojson.type !== "FeatureCollection" ) { feature = { - type: 'Feature', + type: "Feature", geometry: originalOsmGeojson, properties: {}, }; @@ -289,9 +284,9 @@ function initOsmGeojsonGenerator(options = {}) { // Create dashed line style for reference originalBoundaryLayer = L.geoJSON(feature, { style: { - color: '#ff6600', + color: "#ff6600", weight: 2, - dashArray: '5, 5', + dashArray: "5, 5", fillOpacity: 0, interactive: false, }, @@ -308,11 +303,11 @@ function initOsmGeojsonGenerator(options = {}) { // Wrap as Feature if needed let feature = originalOsmGeojson; if ( - originalOsmGeojson.type !== 'Feature' && - originalOsmGeojson.type !== 'FeatureCollection' + originalOsmGeojson.type !== "Feature" && + originalOsmGeojson.type !== "FeatureCollection" ) { feature = { - type: 'Feature', + type: "Feature", geometry: originalOsmGeojson, properties: {}, }; @@ -328,7 +323,7 @@ function initOsmGeojsonGenerator(options = {}) { // Apply buffer if > 0 if (buffer > 0) { processed = turf.buffer(processed, buffer, { - units: 'kilometers', + units: "kilometers", }); } @@ -354,17 +349,12 @@ function initOsmGeojsonGenerator(options = {}) { }); if (!processed) { - console.warn( - '[OsmGeojsonGenerator] Convex hull returned null', - ); + console.warn("[OsmGeojsonGenerator] Convex hull returned null"); // Fallback: try pure convex processed = turf.convex(feature); } } catch (e) { - console.warn( - '[OsmGeojsonGenerator] Convex hull failed:', - e.message, - ); + console.warn("[OsmGeojsonGenerator] Convex hull failed:", e.message); // Keep original processed value } } @@ -379,9 +369,9 @@ function initOsmGeojsonGenerator(options = {}) { // Add preview layer previewLayer = L.geoJSON(processed, { style: { - color: '#3388ff', + color: "#3388ff", weight: 3, - fillColor: '#3388ff', + fillColor: "#3388ff", fillOpacity: 0.2, }, }).addTo(map); @@ -393,10 +383,7 @@ function initOsmGeojsonGenerator(options = {}) { const count = countGeojsonPoints(processed); elements.pointsCount.textContent = `Points: ${count}`; } catch (error) { - console.error( - '[OsmGeojsonGenerator] Error processing GeoJSON:', - error, - ); + console.error("[OsmGeojsonGenerator] Error processing GeoJSON:", error); showError(`Error processing: ${error.message}`); } } @@ -404,12 +391,8 @@ function initOsmGeojsonGenerator(options = {}) { // Apply the processed GeoJSON to the map for editing function applyProcessedGeojson() { if (!processedGeojson) { - if (typeof showToast === 'function') { - showToast( - 'Warning', - 'No processed GeoJSON to apply', - 'warning', - ); + if (typeof showToast === "function") { + showToast("Warning", "No processed GeoJSON to apply", "warning"); } return; } @@ -437,32 +420,32 @@ function initOsmGeojsonGenerator(options = {}) { } // Hide controls and reset state - elements.controls.style.display = 'none'; - elements.resultsContainer.style.display = 'none'; - elements.searchInput.value = ''; + elements.controls.style.display = "none"; + elements.resultsContainer.style.display = "none"; + elements.searchInput.value = ""; elements.resultsSelect.innerHTML = ''; originalOsmGeojson = null; processedGeojson = null; osmSearchResults = []; - if (typeof showToast === 'function') { + if (typeof showToast === "function") { showToast( - 'Success', - 'GeoJSON applied. You can now edit the polygon on the map.', - 'success', + "Success", + "GeoJSON applied. You can now edit the polygon on the map.", + "success", ); } } // Set up event listeners if (elements.searchBtn) { - elements.searchBtn.addEventListener('click', searchOSM); + elements.searchBtn.addEventListener("click", searchOSM); } if (elements.searchInput) { - elements.searchInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { + elements.searchInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { e.preventDefault(); searchOSM(); } @@ -470,11 +453,11 @@ function initOsmGeojsonGenerator(options = {}) { } if (elements.resultsSelect) { - elements.resultsSelect.addEventListener('change', onSearchResultSelect); + elements.resultsSelect.addEventListener("change", onSearchResultSelect); } if (elements.simplifySlider) { - elements.simplifySlider.addEventListener('input', function () { + elements.simplifySlider.addEventListener("input", function () { const tolerance = sliderToTolerance(Number.parseFloat(this.value)); elements.simplifyValue.textContent = formatTolerance(tolerance); processAndPreviewGeojson(); @@ -482,7 +465,7 @@ function initOsmGeojsonGenerator(options = {}) { } if (elements.bufferSlider) { - elements.bufferSlider.addEventListener('input', function () { + elements.bufferSlider.addEventListener("input", function () { elements.bufferValue.textContent = this.value; processAndPreviewGeojson(); }); @@ -490,14 +473,14 @@ function initOsmGeojsonGenerator(options = {}) { if (elements.showOriginalBoundary) { elements.showOriginalBoundary.addEventListener( - 'change', + "change", updateOriginalBoundaryLayer, ); } // Mega simplify checkbox if (elements.megaSimplifyCheckbox) { - elements.megaSimplifyCheckbox.addEventListener('change', function () { + elements.megaSimplifyCheckbox.addEventListener("change", function () { // Enable/disable tightness slider based on checkbox if (elements.tightnessSlider) { elements.tightnessSlider.disabled = !this.checked; @@ -508,7 +491,7 @@ function initOsmGeojsonGenerator(options = {}) { // Tightness slider if (elements.tightnessSlider) { - elements.tightnessSlider.addEventListener('input', function () { + elements.tightnessSlider.addEventListener("input", function () { const val = Number.parseInt(this.value); if (elements.tightnessValue) { elements.tightnessValue.textContent = formatTightness(val); @@ -518,7 +501,7 @@ function initOsmGeojsonGenerator(options = {}) { } if (elements.applyBtn) { - elements.applyBtn.addEventListener('click', applyProcessedGeojson); + elements.applyBtn.addEventListener("click", applyProcessedGeojson); } // Return public API (minimal, mostly self-contained) @@ -534,9 +517,9 @@ function initOsmGeojsonGenerator(options = {}) { map.removeLayer(originalBoundaryLayer); originalBoundaryLayer = null; } - elements.controls.style.display = 'none'; - elements.resultsContainer.style.display = 'none'; - elements.searchInput.value = ''; + elements.controls.style.display = "none"; + elements.resultsContainer.style.display = "none"; + elements.searchInput.value = ""; originalOsmGeojson = null; processedGeojson = null; osmSearchResults = []; @@ -545,6 +528,6 @@ function initOsmGeojsonGenerator(options = {}) { } // Export for module systems if available -if (typeof module !== 'undefined' && module.exports) { +if (typeof module !== "undefined" && module.exports) { module.exports = { initOsmGeojsonGenerator }; } diff --git a/static/js/script.js b/static/js/script.js index 82ecc51..19ff379 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -16,47 +16,47 @@ async function apiFetch(url, options = {}) { const data = await response.clone().json(); if (data.session_expired) { showToast( - 'Session Expired', - 'Your session has expired. Redirecting to login...', - 'warning', + "Session Expired", + "Your session has expired. Redirecting to login...", + "warning", ); setTimeout(() => { window.location.href = `/login?next=${encodeURIComponent(window.location.href)}`; }, 1500); - throw new Error('Session expired'); + throw new Error("Session expired"); } } catch (e) { // If we can't parse JSON or it's our own error, still handle as session expired - if (e.message === 'Session expired') { + if (e.message === "Session expired") { throw e; } // For other parse errors, check if we got redirected to login page const text = await response.clone().text(); - if (text.includes('Login') || response.url.includes('/login')) { + if (text.includes("Login") || response.url.includes("/login")) { showToast( - 'Session Expired', - 'Your session has expired. Redirecting to login...', - 'warning', + "Session Expired", + "Your session has expired. Redirecting to login...", + "warning", ); setTimeout(() => { window.location.href = `/login?next=${encodeURIComponent(window.location.href)}`; }, 1500); - throw new Error('Session expired'); + throw new Error("Session expired"); } } } // Also check if we were redirected to login page (for cases where 401 isn't returned) - if (response.redirected && response.url.includes('/login')) { + if (response.redirected && response.url.includes("/login")) { showToast( - 'Session Expired', - 'Your session has expired. Redirecting to login...', - 'warning', + "Session Expired", + "Your session has expired. Redirecting to login...", + "warning", ); setTimeout(() => { window.location.href = `/login?next=${encodeURIComponent(window.location.href)}`; }, 1500); - throw new Error('Session expired'); + throw new Error("Session expired"); } return response; @@ -70,9 +70,9 @@ function editTag(areaId, tagName, tagValue) { } function addTag(areaId) { - const tagName = prompt('Enter tag name:'); + const tagName = prompt("Enter tag name:"); if (tagName) { - const tagValue = prompt('Enter tag value:'); + const tagValue = prompt("Enter tag value:"); if (tagValue) { setAreaTag(areaId, tagName, tagValue); } @@ -80,10 +80,10 @@ function addTag(areaId) { } function setAreaTag(areaId, tagName, tagValue) { - apiFetch('/api/set_area_tag', { - method: 'POST', + apiFetch("/api/set_area_tag", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ id: areaId, name: tagName, value: tagValue }), }) @@ -91,26 +91,26 @@ function setAreaTag(areaId, tagName, tagValue) { .then((data) => { if (data.error) { showToast( - 'Error', - data.error.message || 'Failed to update tag', - 'error', + "Error", + data.error.message || "Failed to update tag", + "error", ); } else { location.reload(); } }) .catch((error) => { - if (error.message !== 'Session expired') { - showToast('Error', 'Failed to update tag', 'error'); + if (error.message !== "Session expired") { + showToast("Error", "Failed to update tag", "error"); } }); } function removeTag(areaId, tagName) { - apiFetch('/api/remove_area_tag', { - method: 'POST', + apiFetch("/api/remove_area_tag", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ id: areaId, tag: tagName }), }) @@ -118,27 +118,27 @@ function removeTag(areaId, tagName) { .then((data) => { if (data.error) { showToast( - 'Error', - data.error.message || 'Failed to remove tag', - 'error', + "Error", + data.error.message || "Failed to remove tag", + "error", ); } else { - showToast('Success', 'Tag removed successfully', 'success'); + showToast("Success", "Tag removed successfully", "success"); setTimeout(() => location.reload(), 1000); } }) .catch((error) => { - if (error.message !== 'Session expired') { - showToast('Error', 'Failed to remove tag', 'error'); + if (error.message !== "Session expired") { + showToast("Error", "Failed to remove tag", "error"); } }); } function removeArea(areaId) { - apiFetch('/api/remove_area', { - method: 'POST', + apiFetch("/api/remove_area", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ id: areaId }), }) @@ -146,18 +146,18 @@ function removeArea(areaId) { .then((data) => { if (data.error) { showToast( - 'Error', - data.error.message || 'Failed to remove area', - 'error', + "Error", + data.error.message || "Failed to remove area", + "error", ); } else { - showToast('Success', 'Area removed successfully', 'success'); - setTimeout(() => (window.location.href = '/select_area'), 1500); + showToast("Success", "Area removed successfully", "success"); + setTimeout(() => (window.location.href = "/select_area"), 1500); } }) .catch((error) => { - if (error.message !== 'Session expired') { - showToast('Error', 'Failed to remove area', 'error'); + if (error.message !== "Session expired") { + showToast("Error", "Failed to remove area", "error"); } }); } diff --git a/static/js/validation.js b/static/js/validation.js index cbe9346..e275f11 100644 --- a/static/js/validation.js +++ b/static/js/validation.js @@ -1,55 +1,54 @@ // Common validation functions function validateKey(key, existingKeys) { - key = key ? key.trim() : ''; + key = key ? key.trim() : ""; if (!key) { - return { isValid: false, message: 'Key cannot be empty' }; + return { isValid: false, message: "Key cannot be empty" }; } if (existingKeys?.includes(key)) { - return { isValid: false, message: 'Key already exists' }; + return { isValid: false, message: "Key already exists" }; } if (!/^[a-zA-Z][a-zA-Z0-9_:]*$/.test(key)) { return { isValid: false, message: - 'Key must start with a letter and contain only letters, numbers, underscores, and colons', + "Key must start with a letter and contain only letters, numbers, underscores, and colons", }; } return { isValid: true }; } function validateNumericValue(value, type) { - if (!value || value.trim() === '') { - return { isValid: false, message: 'Value cannot be empty' }; + if (!value || value.trim() === "") { + return { isValid: false, message: "Value cannot be empty" }; } value = value.toString().trim(); - if (type === 'integer') { + if (type === "integer") { if (!/^\d+$/.test(value)) { return { isValid: false, - message: 'Value must be a valid integer (no decimal points)', + message: "Value must be a valid integer (no decimal points)", }; } const num = Number.parseInt(value, 10); if (num < 0) { - return { isValid: false, message: 'Value must be non-negative' }; + return { isValid: false, message: "Value must be non-negative" }; } return { isValid: true, value: num }; - } else if (type === 'number') { + } else if (type === "number") { if (!/^\d*\.?\d*$/.test(value)) { return { isValid: false, - message: - 'Value must contain only digits and at most one decimal point', + message: "Value must contain only digits and at most one decimal point", }; } const num = Number.parseFloat(value); if (Number.isNaN(num)) { - return { isValid: false, message: 'Value must be a valid number' }; + return { isValid: false, message: "Value must be a valid number" }; } if (num < 0) { - return { isValid: false, message: 'Value must be non-negative' }; + return { isValid: false, message: "Value must be non-negative" }; } return { isValid: true, value: num }; } @@ -58,18 +57,18 @@ function validateNumericValue(value, type) { } function validateValue(value, requirements) { - if (!value || value.trim() === '') { - return { isValid: false, message: 'Value cannot be empty' }; + if (!value || value.trim() === "") { + return { isValid: false, message: "Value cannot be empty" }; } if (requirements?.type) { - if (requirements.type === 'integer' || requirements.type === 'number') { + if (requirements.type === "integer" || requirements.type === "number") { return validateNumericValue(value, requirements.type); } else if (requirements.allowed_values) { if (!requirements.allowed_values.includes(value)) { return { isValid: false, - message: `Value must be one of: ${requirements.allowed_values.join(', ')}`, + message: `Value must be one of: ${requirements.allowed_values.join(", ")}`, }; } } From b64fa581f6cd4edd4a3410609267efd3766415e0 Mon Sep 17 00:00:00 2001 From: escapedcat Date: Sat, 21 Feb 2026 12:52:28 +0100 Subject: [PATCH 5/5] chore: track package-lock.json for npm ci --- .gitignore | 1 - package-lock.json | 179 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index d670b72..d99ae3e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ env/ # Node.js node_modules/ -package-lock.json # Biome .biome/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ec65e1e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,179 @@ +{ + "name": "btcmap-admin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "btcmap-admin", + "version": "0.1.0", + "devDependencies": { + "@biomejs/biome": "^1.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + } + } +}