// Configuration const CONFIG = { REQUIRED_EXCEL_COLUMNS: ['field', 'sub_field', 'year', 'season_start', 'season_end', 'tonnage_ha'], OPTIONAL_EXCEL_COLUMNS: ['age', 'sub_area'], // age is calculated in script, sub_area is optional REQUIRED_GEOJSON_PROPERTIES: ['field', 'sub_field'], VALID_CRS: 'EPSG:32736', // UTM 36S - the correct CRS we learned from the conversation CRS_DESCRIPTION: 'EPSG:32736 (UTM Zone 36S) - This is the correct CRS learned from geospatial analysis of Angata farm coordinates' }; let excelData = null; let geojsonData = null; let excelLoaded = false; let geojsonLoaded = false; // File input handlers document.getElementById('excelFile').addEventListener('change', handleExcelFile); document.getElementById('geojsonFile').addEventListener('change', handleGeojsonFile); document.getElementById('checkButton').addEventListener('click', validateData); function updateCheckButton() { const checkButton = document.getElementById('checkButton'); if (excelLoaded && geojsonLoaded) { checkButton.style.display = 'inline-block'; } else { checkButton.style.display = 'none'; } } // Drag and drop handlers for Excel const excelDropZone = document.getElementById('excelDropZone'); excelDropZone.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); excelDropZone.style.backgroundColor = '#f0f1ff'; }); excelDropZone.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); excelDropZone.style.backgroundColor = 'transparent'; }); excelDropZone.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); excelDropZone.style.backgroundColor = 'transparent'; const files = e.dataTransfer.files; if (files.length > 0) { document.getElementById('excelFile').files = files; handleExcelFile({ target: { files: files } }); } }); // Drag and drop handlers for GeoJSON const geojsonDropZone = document.getElementById('geojsonDropZone'); geojsonDropZone.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); geojsonDropZone.style.backgroundColor = '#f0f1ff'; }); geojsonDropZone.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); geojsonDropZone.style.backgroundColor = 'transparent'; }); geojsonDropZone.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); geojsonDropZone.style.backgroundColor = 'transparent'; const files = e.dataTransfer.files; if (files.length > 0) { document.getElementById('geojsonFile').files = files; handleGeojsonFile({ target: { files: files } }); } }); function handleExcelFile(e) { const file = e.target.files[0]; if (!file) return; document.getElementById('excelFileName').textContent = `✓ ${file.name}`; const reader = new FileReader(); reader.onload = (event) => { try { const data = new Uint8Array(event.target.result); const workbook = XLSX.read(data, { type: 'array' }); const worksheet = workbook.Sheets[workbook.SheetNames[0]]; excelData = XLSX.utils.sheet_to_json(worksheet); excelLoaded = true; updateCheckButton(); } catch (error) { document.getElementById('excelFileName').textContent = `✗ Error: ${error.message}`; excelLoaded = false; updateCheckButton(); } }; reader.onerror = () => { document.getElementById('excelFileName').textContent = `✗ Failed to read file`; excelLoaded = false; updateCheckButton(); }; reader.readAsArrayBuffer(file); } function handleGeojsonFile(e) { const file = e.target.files[0]; if (!file) return; document.getElementById('geojsonFileName').textContent = `✓ ${file.name}`; const reader = new FileReader(); reader.onload = (event) => { try { geojsonData = JSON.parse(event.target.result); geojsonLoaded = true; updateCheckButton(); } catch (error) { document.getElementById('geojsonFileName').textContent = `✗ Invalid JSON: ${error.message}`; geojsonLoaded = false; updateCheckButton(); } }; reader.onerror = () => { document.getElementById('geojsonFileName').textContent = `✗ Failed to read file`; geojsonLoaded = false; updateCheckButton(); }; reader.readAsText(file); } function validateData() { if (!excelData || !geojsonData) { alert('Please upload both Excel and GeoJSON files before checking.'); return; } const results = { checks: [], details: [] }; // 1. Excel column validation const excelColumnCheck = validateExcelColumns(); results.checks.push(excelColumnCheck); results.details.push(excelColumnCheck.details); // 2. GeoJSON properties validation const geojsonPropsCheck = validateGeojsonProperties(); results.checks.push(geojsonPropsCheck); results.details.push(geojsonPropsCheck.details); // 3. CRS validation const crsCheck = validateCRS(); results.checks.push(crsCheck); results.details.push(crsCheck.details); // 4. Field name matching const fieldMatchCheck = validateFieldMatching(); results.checks.push(fieldMatchCheck); results.details.push(fieldMatchCheck.details); // 5. Data type and content validation const dataValidationCheck = validateDataTypes(); results.checks.push(dataValidationCheck); results.details.push(dataValidationCheck.details); displayResults(results); } function validateExcelColumns() { const excelColumns = Object.keys(excelData[0] || {}); const missing = CONFIG.REQUIRED_EXCEL_COLUMNS.filter(col => !excelColumns.includes(col)); const hasOptional = CONFIG.OPTIONAL_EXCEL_COLUMNS.filter(col => excelColumns.includes(col)); const notRequired = excelColumns.filter(col => !CONFIG.REQUIRED_EXCEL_COLUMNS.includes(col) && !CONFIG.OPTIONAL_EXCEL_COLUMNS.includes(col)); let status = 'pass'; let message = 'All required columns present'; if (missing.length > 0) { status = 'fail'; message = `Missing required columns: ${missing.join(', ')}`; } else if (notRequired.length > 0) { status = 'warning'; message = `Extra columns detected (will be ignored): ${notRequired.join(', ')}`; } return { name: 'Excel Columns', status: status, message: message, details: { title: 'Excel Column Validation', type: 'columns', required: CONFIG.REQUIRED_EXCEL_COLUMNS, optional: CONFIG.OPTIONAL_EXCEL_COLUMNS, found: excelColumns, missing: missing, hasOptional: hasOptional, extra: notRequired } }; } function validateGeojsonProperties() { if (!geojsonData.features || geojsonData.features.length === 0) { return { name: 'GeoJSON Properties', status: 'fail', message: 'GeoJSON has no features', details: { title: 'GeoJSON Property Validation', type: 'properties', error: 'No features found in GeoJSON' } }; } const allProperties = new Set(); const missingInFeatures = []; geojsonData.features.forEach((feature, idx) => { const props = feature.properties || {}; Object.keys(props).forEach(p => allProperties.add(p)); CONFIG.REQUIRED_GEOJSON_PROPERTIES.forEach(reqProp => { if (!props[reqProp]) { missingInFeatures.push({ feature: idx, property: reqProp, field: props.field || 'Unknown' }); } }); }); const extra = Array.from(allProperties).filter(p => !CONFIG.REQUIRED_GEOJSON_PROPERTIES.includes(p)); let status = 'pass'; let message = 'All required properties present in all features'; if (missingInFeatures.length > 0) { status = 'fail'; message = `Missing properties in ${missingInFeatures.length} feature(s)`; } else if (extra.length > 0) { status = 'warning'; message = `Extra properties detected: ${extra.join(', ')}`; } return { name: 'GeoJSON Properties', status: status, message: message, details: { title: 'GeoJSON Property Validation', type: 'properties', required: CONFIG.REQUIRED_GEOJSON_PROPERTIES, found: Array.from(allProperties), extra: extra, missingInFeatures: missingInFeatures } }; } function validateCRS() { const crs = geojsonData.crs; let detectedCRS = 'Not specified'; let status = 'fail'; let message = `CRS not specified. Expected: ${CONFIG.VALID_CRS}`; if (crs) { if (crs.type === 'name' && crs.properties?.name) { detectedCRS = crs.properties.name; // Check for various CRS string formats if (detectedCRS.includes('32736') || detectedCRS.includes('UTM') && detectedCRS.includes('36')) { status = 'pass'; message = `✓ Correct CRS detected: ${detectedCRS}`; } else { status = 'fail'; message = `Wrong CRS: ${detectedCRS}. Expected: ${CONFIG.VALID_CRS}`; } } } return { name: 'Coordinate Reference System', status: status, message: message, details: { title: 'CRS Validation', type: 'crs', expected: CONFIG.VALID_CRS, description: CONFIG.CRS_DESCRIPTION, detected: detectedCRS, crsObject: crs } }; } function validateFieldMatching() { const excelFields = new Set(excelData.map(row => String(row.field).trim())); const geojsonFields = new Set(geojsonData.features.map(f => String(f.properties.field).trim())); const matchingFields = Array.from(excelFields).filter(f => geojsonFields.has(f)); const excelOnly = Array.from(excelFields).filter(f => !geojsonFields.has(f)); const geojsonOnly = Array.from(geojsonFields).filter(f => !excelFields.has(f)); let status = 'pass'; let message = 'All field names match between Excel and GeoJSON'; if (excelOnly.length > 0 || geojsonOnly.length > 0) { status = 'fail'; message = `Field name mismatches detected: ${excelOnly.length} in Excel only, ${geojsonOnly.length} in GeoJSON only`; } // Create matching table const matchingTable = []; excelFields.forEach(field => { const inGeojson = geojsonFields.has(field); matchingTable.push({ field: field, excel: true, geojson: inGeojson, status: inGeojson ? 'match' : 'mismatch' }); }); geojsonOnly.forEach(field => { matchingTable.push({ field: field, excel: false, geojson: true, status: 'mismatch' }); }); return { name: 'Field Name Matching', status: status, message: message, details: { title: 'Field Name Matching', type: 'fieldMatching', matching: matchingFields, excelOnly: excelOnly, geojsonOnly: geojsonOnly, matchingTable: matchingTable } }; } function validateDataTypes() { const issues = []; const missingDates = []; const invalidYears = []; const invalidNumerics = []; excelData.forEach((row, idx) => { // Check season_start if (!row.season_start || row.season_start === '') { missingDates.push({ row: idx + 2, field: row.field, column: 'season_start' }); } else if (!isValidDate(row.season_start)) { invalidYears.push({ row: idx + 2, field: row.field, column: 'season_start', value: row.season_start }); } // Check year if (!Number.isInteger(parseFloat(row.year))) { invalidYears.push({ row: idx + 2, field: row.field, column: 'year', value: row.year }); } // Check numeric columns (age is optional, sub_area is text, not numeric) ['tonnage_ha'].forEach(col => { const val = row[col]; if (val !== '' && val !== null && isNaN(parseFloat(val))) { invalidNumerics.push({ row: idx + 2, field: row.field, column: col, value: val }); } }); }); let status = 'pass'; let message = 'All data types valid'; if (missingDates.length > 0 || invalidYears.length > 0 || invalidNumerics.length > 0) { status = 'warning'; message = `Data validation issues found: ${missingDates.length} missing dates, ${invalidYears.length} invalid years/dates, ${invalidNumerics.length} invalid numerics`; } return { name: 'Data Validation', status: status, message: message, details: { title: 'Data Type & Content Validation', type: 'dataValidation', missingDates: missingDates, invalidYears: invalidYears, invalidNumerics: invalidNumerics } }; } function isValidDate(dateString) { if (!dateString) return false; const date = new Date(dateString); return date instanceof Date && !isNaN(date); } function displayResults(results) { const trafficLight = document.getElementById('trafficLight'); const detailsSection = document.getElementById('detailsSection'); const resultsSection = document.getElementById('resultsSection'); trafficLight.innerHTML = ''; detailsSection.innerHTML = ''; // Display traffic lights results.checks.forEach(check => { const light = document.createElement('div'); light.className = `check-item ${check.status}`; light.innerHTML = `
${check.name}
${check.message}
`; trafficLight.appendChild(light); }); // Display details results.details.forEach(detail => { if (detail.type === 'columns') { detailsSection.appendChild(createColumnDetails(detail)); } else if (detail.type === 'properties') { detailsSection.appendChild(createPropertiesDetails(detail)); } else if (detail.type === 'crs') { detailsSection.appendChild(createCRSDetails(detail)); } else if (detail.type === 'fieldMatching') { detailsSection.appendChild(createFieldMatchingDetails(detail)); } else if (detail.type === 'dataValidation') { detailsSection.appendChild(createDataValidationDetails(detail)); } }); resultsSection.classList.add('show'); } function createColumnDetails(detail) { const section = document.createElement('div'); section.innerHTML = `

${detail.title}

`; // Required columns section.innerHTML += `
Required Columns:
${detail.required.map(col => `
${col}
`).join('')}
`; // Optional columns if (detail.optional && detail.optional.length > 0) { section.innerHTML += `
Optional Columns (not required):
${detail.optional.map(col => `
${col}
`).join('')}
${detail.optional.join(', ')} ${detail.optional.length === 1 ? 'is' : 'are'} calculated in the system or optional
`; } if (detail.missing.length > 0) { section.innerHTML += `
❌ Missing Required Columns:
${detail.missing.join(', ')}
`; } if (detail.extra.length > 0) { section.innerHTML += `
⚠️ Extra Columns (will be ignored):
${detail.extra.join(', ')}
`; } if (detail.missing.length === 0 && detail.extra.length === 0) { section.innerHTML += `
✓ Perfect! All required columns present.
`; } return section; } function createPropertiesDetails(detail) { const section = document.createElement('div'); section.innerHTML = `

${detail.title}

`; if (detail.error) { section.innerHTML += `
${detail.error}
`; return section; } if (detail.missingInFeatures && detail.missingInFeatures.length > 0) { section.innerHTML += `
❌ Missing Properties in Features: ${detail.missingInFeatures.map(m => ``).join('')}
Feature #Field NameMissing Property
${m.feature}${m.field}${m.property}
`; } if (detail.extra && detail.extra.length > 0) { section.innerHTML += `
⚠️ Extra Properties (redundant):
${detail.extra.join(', ')}
These will be ignored during processing.
`; } if ((!detail.missingInFeatures || detail.missingInFeatures.length === 0) && (!detail.extra || detail.extra.length === 0)) { section.innerHTML += `
✓ Perfect! All required properties present in all ${geojsonData.features.length} features.
`; } return section; } function createCRSDetails(detail) { const section = document.createElement('div'); section.innerHTML = `

${detail.title}

`; if (detail.detected === 'Not specified') { section.innerHTML += `
❌ CRS Not Specified
Expected: ${detail.expected}
${detail.description}
`; } else if (detail.detected.includes('32736') || (detail.detected.includes('UTM') && detail.detected.includes('36'))) { section.innerHTML += `
✓ Correct CRS
Detected: ${detail.detected}
${detail.description}
`; } else { section.innerHTML += `
❌ Wrong CRS
Expected: ${detail.expected}
Detected: ${detail.detected}
${detail.description}
`; } if (detail.crsObject) { section.innerHTML += `
CRS Details:
${JSON.stringify(detail.crsObject, null, 2)}
`; } return section; } function createFieldMatchingDetails(detail) { const section = document.createElement('div'); section.innerHTML = `

${detail.title}

`; if (detail.excelOnly.length > 0) { section.innerHTML += `
❌ Fields in Excel but NOT in GeoJSON (${detail.excelOnly.length}):
${detail.excelOnly.map(f => `
${f}
`).join('')}
These fields exist in your harvest data but have no boundaries defined in the GeoJSON.
`; } if (detail.geojsonOnly.length > 0) { section.innerHTML += `
❌ Fields in GeoJSON but NOT in Excel (${detail.geojsonOnly.length}):
${detail.geojsonOnly.map(f => `
${f}
`).join('')}
These fields have boundaries defined but no data in your harvest spreadsheet.
`; } if (detail.matching.length > 0) { section.innerHTML += `
✓ Matching Fields (${detail.matching.length}):
${detail.matching.map(f => `
${f}
`).join('')}
`; } // Full matching table section.innerHTML += `
Complete Field Summary: ${detail.matchingTable.map(row => ` `).join('')}
Field Name In Excel In GeoJSON Status
${row.field} ${row.excel ? '✓' : '✗'} ${row.geojson ? '✓' : '✗'} ${row.status === 'match' ? '🟢 Match' : '🔴 Mismatch'}
`; return section; } function createDataValidationDetails(detail) { const section = document.createElement('div'); section.innerHTML = `

${detail.title}

`; if (detail.missingDates.length > 0) { section.innerHTML += `
⚠️ Missing season_start dates (${detail.missingDates.length}): ${detail.missingDates.map(m => ``).join('')}
Row #Field Name
${m.row}${m.field}
`; } if (detail.invalidYears.length > 0) { section.innerHTML += `
⚠️ Invalid dates/years (${detail.invalidYears.length}): ${detail.invalidYears.map(m => ``).join('')}
Row #Field NameColumnValue
${m.row}${m.field}${m.column}${m.value}
`; } if (detail.invalidNumerics.length > 0) { section.innerHTML += `
⚠️ Invalid numeric values (${detail.invalidNumerics.length}): ${detail.invalidNumerics.map(m => ``).join('')}
Row #Field NameColumnValue
${m.row}${m.field}${m.column}${m.value}
`; } if (detail.missingDates.length === 0 && detail.invalidYears.length === 0 && detail.invalidNumerics.length === 0) { section.innerHTML += `
✓ All data types valid! No missing dates or invalid values detected.
`; } return section; } function showError(fileType, message) { alert(`${fileType} Error: ${message}`); }