diff --git a/web/app.js b/web/app.js
new file mode 100644
index 0000000..941c812
--- /dev/null
+++ b/web/app.js
@@ -0,0 +1,454 @@
+// Global state
+let map;
+let osmLayer;
+let diffLayer;
+let countyLayer;
+let osmData = null;
+let diffData = null;
+let countyData = null;
+let selectedFeature = null;
+let selectedLayer = null;
+let acceptedFeatures = new Set();
+let rejectedFeatures = new Set();
+let featurePopup = null;
+
+// Initialize map
+function initMap() {
+ map = L.map('map').setView([28.7, -81.7], 12);
+
+ L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
+ attribution: '© OpenStreetMap contributors © CARTO',
+ subdomains: 'abcd',
+ maxZoom: 20
+ }).addTo(map);
+}
+
+// Calculate bounds for all loaded layers
+function calculateBounds() {
+ const bounds = L.latLngBounds([]);
+ let hasData = false;
+
+ if (osmData && osmData.features.length > 0) {
+ L.geoJSON(osmData).eachLayer(layer => {
+ if (layer.getBounds) {
+ bounds.extend(layer.getBounds());
+ } else if (layer.getLatLng) {
+ bounds.extend(layer.getLatLng());
+ }
+ });
+ hasData = true;
+ }
+
+ if (diffData && diffData.features.length > 0) {
+ L.geoJSON(diffData).eachLayer(layer => {
+ if (layer.getBounds) {
+ bounds.extend(layer.getBounds());
+ } else if (layer.getLatLng) {
+ bounds.extend(layer.getLatLng());
+ }
+ });
+ hasData = true;
+ }
+
+ if (countyData && countyData.features.length > 0) {
+ L.geoJSON(countyData).eachLayer(layer => {
+ if (layer.getBounds) {
+ bounds.extend(layer.getBounds());
+ } else if (layer.getLatLng) {
+ bounds.extend(layer.getLatLng());
+ }
+ });
+ hasData = true;
+ }
+
+ if (hasData && bounds.isValid()) {
+ map.fitBounds(bounds, { padding: [50, 50] });
+ }
+}
+
+// Style functions
+function osmStyle(feature) {
+ return {
+ color: '#8B4513',
+ weight: 3,
+ opacity: 0.7
+ };
+}
+
+function diffStyle(feature) {
+ // Check if feature is accepted or rejected
+ if (acceptedFeatures.has(feature)) {
+ return {
+ color: '#007bff',
+ weight: 3,
+ opacity: 0.8
+ };
+ }
+
+ if (rejectedFeatures.has(feature)) {
+ return {
+ color: '#4a4a4a',
+ weight: 3,
+ opacity: 0.8
+ };
+ }
+
+ const isRemoved = feature.properties && feature.properties.removed === true;
+ return {
+ color: isRemoved ? '#dc3545' : '#28a745',
+ weight: 3,
+ opacity: 0.8
+ };
+}
+
+function countyStyle(feature) {
+ return {
+ color: '#800080',
+ weight: 3,
+ opacity: 0.7
+ };
+}
+
+// Create layer for OSM data
+function createOsmLayer() {
+ if (osmLayer) {
+ map.removeLayer(osmLayer);
+ }
+
+ if (!osmData) return;
+
+ osmLayer = L.geoJSON(osmData, {
+ style: osmStyle
+ }).addTo(map);
+}
+
+// Create layer for diff data with click handlers
+function createDiffLayer() {
+ if (diffLayer) {
+ map.removeLayer(diffLayer);
+ }
+
+ if (!diffData) return;
+
+ diffLayer = L.geoJSON(diffData, {
+ style: diffStyle,
+ onEachFeature: function(feature, layer) {
+ layer.on('click', function(e) {
+ L.DomEvent.stopPropagation(e);
+ selectFeature(feature, layer, e);
+ });
+
+ layer.on('mouseover', function(e) {
+ if (selectedLayer !== layer) {
+ layer.setStyle({
+ weight: 5,
+ opacity: 1
+ });
+ }
+ });
+
+ layer.on('mouseout', function(e) {
+ if (selectedLayer !== layer) {
+ layer.setStyle(diffStyle(feature));
+ }
+ });
+ }
+ }).addTo(map);
+}
+
+// Create layer for county data
+function createCountyLayer() {
+ if (countyLayer) {
+ map.removeLayer(countyLayer);
+ }
+
+ if (!countyData) return;
+
+ countyLayer = L.geoJSON(countyData, {
+ style: countyStyle
+ });
+
+ // County layer is hidden by default
+ if (document.getElementById('countyToggle').checked) {
+ countyLayer.addTo(map);
+ }
+}
+
+// Select a feature from diff layer
+function selectFeature(feature, layer, e) {
+ // Deselect previous feature
+ if (selectedLayer) {
+ selectedLayer.setStyle(diffStyle(selectedLayer.feature));
+ }
+
+ selectedFeature = feature;
+ selectedLayer = layer;
+ layer.setStyle({
+ weight: 6,
+ opacity: 1,
+ color: '#ffc107'
+ });
+
+ // Create popup near the clicked location
+ const props = feature.properties || {};
+ const isRemoved = props.removed === true;
+ const isAccepted = acceptedFeatures.has(feature);
+ const isRejected = rejectedFeatures.has(feature);
+
+ let html = '
';
+ html += `
Status: ${isRemoved ? 'Removed' : 'Added/Modified'}
`;
+
+ if (props.name) {
+ html += `
Name: ${props.name}
`;
+ }
+ if (props.highway) {
+ html += `
Highway: ${props.highway}
`;
+ }
+ if (props.service) {
+ html += `
Service: ${props.service}
`;
+ }
+
+ if (isAccepted) {
+ html += '
✓ Accepted
';
+ } else if (isRejected) {
+ html += '
✗ Rejected
';
+ }
+
+ html += '
';
+ html += '';
+ html += '';
+ html += '
';
+ html += '
';
+
+ // Remove old popup if exists
+ if (featurePopup) {
+ map.closePopup(featurePopup);
+ }
+
+ // Create popup at click location
+ featurePopup = L.popup({
+ maxWidth: 300,
+ closeButton: true,
+ autoClose: false,
+ closeOnClick: false
+ })
+ .setLatLng(e.latlng)
+ .setContent(html)
+ .openOn(map);
+
+ // Handle popup close
+ featurePopup.on('remove', function() {
+ if (selectedLayer) {
+ selectedLayer.setStyle(diffStyle(selectedLayer.feature));
+ selectedLayer = null;
+ selectedFeature = null;
+ }
+ });
+}
+
+// Accept a feature
+function acceptFeature() {
+ if (!selectedFeature) return;
+
+ // Remove from rejected if present
+ rejectedFeatures.delete(selectedFeature);
+
+ // Add to accepted
+ acceptedFeatures.add(selectedFeature);
+
+ // Update layer style
+ if (selectedLayer) {
+ selectedLayer.setStyle(diffStyle(selectedFeature));
+ }
+
+ // Close popup
+ if (featurePopup) {
+ map.closePopup(featurePopup);
+ }
+
+ // Enable save button
+ updateSaveButton();
+
+ showStatus(`${acceptedFeatures.size} accepted, ${rejectedFeatures.size} rejected`, 'success');
+}
+
+// Reject a feature
+function rejectFeature() {
+ if (!selectedFeature) return;
+
+ // Remove from accepted if present
+ acceptedFeatures.delete(selectedFeature);
+
+ // Add to rejected
+ rejectedFeatures.add(selectedFeature);
+
+ // Update layer style
+ if (selectedLayer) {
+ selectedLayer.setStyle(diffStyle(selectedFeature));
+ }
+
+ // Close popup
+ if (featurePopup) {
+ map.closePopup(featurePopup);
+ }
+
+ // Enable save button
+ updateSaveButton();
+
+ showStatus(`${acceptedFeatures.size} accepted, ${rejectedFeatures.size} rejected`, 'success');
+}
+
+// Update save button state
+function updateSaveButton() {
+ document.getElementById('saveButton').disabled =
+ acceptedFeatures.size === 0 && rejectedFeatures.size === 0;
+}
+
+// Load file from input
+function loadFile(input) {
+ return new Promise((resolve, reject) => {
+ const file = input.files[0];
+ if (!file) {
+ resolve(null);
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ try {
+ const data = JSON.parse(e.target.result);
+ resolve(data);
+ } catch (error) {
+ reject(new Error(`Failed to parse ${file.name}: ${error.message}`));
+ }
+ };
+ reader.onerror = () => reject(new Error(`Failed to read ${file.name}`));
+ reader.readAsText(file);
+ });
+}
+
+// Load all files
+async function loadFiles() {
+ try {
+ showStatus('Loading files...', 'success');
+
+ const osmInput = document.getElementById('osmFile');
+ const diffInput = document.getElementById('diffFile');
+ const countyInput = document.getElementById('countyFile');
+
+ // Load files
+ osmData = await loadFile(osmInput);
+ diffData = await loadFile(diffInput);
+ countyData = await loadFile(countyInput);
+
+ if (!osmData && !diffData && !countyData) {
+ showStatus('Please select at least one file', 'error');
+ return;
+ }
+
+ // Create layers
+ createOsmLayer();
+ createDiffLayer();
+ createCountyLayer();
+
+ // Fit bounds to smallest layer
+ calculateBounds();
+
+ showStatus('Files loaded successfully!', 'success');
+
+ // Enable save button if we have diff data
+ document.getElementById('saveButton').disabled = !diffData;
+
+ } catch (error) {
+ showStatus(error.message, 'error');
+ console.error(error);
+ }
+}
+
+// Save accepted and rejected items to original diff file
+async function saveAcceptedItems() {
+ if (!diffData || (acceptedFeatures.size === 0 && rejectedFeatures.size === 0)) {
+ showStatus('No features to save', 'error');
+ return;
+ }
+
+ try {
+ // Add accepted=true or accepted=false property to features
+ diffData.features.forEach(feature => {
+ if (acceptedFeatures.has(feature)) {
+ feature.properties.accepted = true;
+ } else if (rejectedFeatures.has(feature)) {
+ feature.properties.accepted = false;
+ }
+ });
+
+ // Create download
+ const dataStr = JSON.stringify(diffData, null, 2);
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(dataBlob);
+
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'diff-updated.geojson';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+
+ showStatus(`Saved ${acceptedFeatures.size} accepted, ${rejectedFeatures.size} rejected`, 'success');
+
+ } catch (error) {
+ showStatus(`Save failed: ${error.message}`, 'error');
+ console.error(error);
+ }
+}
+
+// Show status message
+function showStatus(message, type) {
+ const status = document.getElementById('status');
+ status.textContent = message;
+ status.className = `status ${type}`;
+
+ setTimeout(() => {
+ status.classList.add('hidden');
+ }, 3000);
+}
+
+// Toggle layer visibility
+function toggleLayer(layerId, layer) {
+ const checkbox = document.getElementById(layerId);
+
+ if (checkbox.checked && layer) {
+ if (!map.hasLayer(layer)) {
+ map.addLayer(layer);
+ }
+ } else if (layer) {
+ if (map.hasLayer(layer)) {
+ map.removeLayer(layer);
+ }
+ }
+}
+
+// Event listeners
+document.addEventListener('DOMContentLoaded', function() {
+ initMap();
+
+ // Layer toggles
+ document.getElementById('osmToggle').addEventListener('change', function() {
+ toggleLayer('osmToggle', osmLayer);
+ });
+
+ document.getElementById('diffToggle').addEventListener('change', function() {
+ toggleLayer('diffToggle', diffLayer);
+ });
+
+ document.getElementById('countyToggle').addEventListener('change', function() {
+ toggleLayer('countyToggle', countyLayer);
+ });
+
+ // Load button
+ document.getElementById('loadButton').addEventListener('click', loadFiles);
+
+ // Save button
+ document.getElementById('saveButton').addEventListener('click', saveAcceptedItems);
+});
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..b0f2d88
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,172 @@
+
+
+
+
+
+ GeoJSON Map Viewer
+
+
+
+
+
+
+
+
+
+
+
+